From 7b97b8abd258c9019acf001072240e10ab3c62eb Mon Sep 17 00:00:00 2001 From: Max Bowsher Date: Sun, 19 Jun 2022 17:38:04 +0100 Subject: [PATCH 001/339] Delete definition of metric `consul.acl.blocked.node.registration` Although the metric is defined, there is no code which ever sets its value - the code in question is genuinely asymmetric - there are 3 types of object for which registration can be tracked, but only 2 for which deregistration can be tracked. --- agent/local/state.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/agent/local/state.go b/agent/local/state.go index 74641a0683..b95cec5a60 100644 --- a/agent/local/state.go +++ b/agent/local/state.go @@ -46,10 +46,6 @@ var StateCounters = []prometheus.CounterDefinition{ Name: []string{"acl", "blocked", "node", "registration"}, Help: "Increments whenever a registration fails for a node (blocked by an ACL)", }, - { - Name: []string{"acl", "blocked", "node", "deregistration"}, - Help: "Increments whenever a deregistration fails for a node (blocked by an ACL)", - }, } const fullSyncReadMaxStale = 2 * time.Second From 952ebb7b9383afe54bea60e9860daba6d39f0b1b Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 5 Jul 2022 17:53:56 -0500 Subject: [PATCH 002/339] restructure documentation --- .../api-gateway/configuration/gateway.mdx | 316 +++++++++++++ .../configuration/gatewayclass.mdx | 37 ++ .../configuration/gatewayclassconfig.mdx | 57 +++ .../docs/api-gateway/configuration/index.mdx | 17 + .../docs/api-gateway/configuration/routes.mdx | 89 ++++ .../consul-api-gateway-install.mdx | 429 +++--------------- website/content/docs/api-gateway/index.mdx | 2 +- .../content/docs/api-gateway/tech-specs.mdx | 70 --- ...ade-specific-versions.mdx => upgrades.mdx} | 0 .../docs/api-gateway/usage/basic-usage.mdx | 131 ++++++ website/data/docs-nav-data.json | 44 +- 11 files changed, 741 insertions(+), 451 deletions(-) create mode 100644 website/content/docs/api-gateway/configuration/gateway.mdx create mode 100644 website/content/docs/api-gateway/configuration/gatewayclass.mdx create mode 100644 website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx create mode 100644 website/content/docs/api-gateway/configuration/index.mdx create mode 100644 website/content/docs/api-gateway/configuration/routes.mdx delete mode 100644 website/content/docs/api-gateway/tech-specs.mdx rename website/content/docs/api-gateway/{upgrade-specific-versions.mdx => upgrades.mdx} (100%) create mode 100644 website/content/docs/api-gateway/usage/basic-usage.mdx diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx new file mode 100644 index 0000000000..c1dd183268 --- /dev/null +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -0,0 +1,316 @@ +--- +layout: docs +page_title: Consul API Gateway Gateway +description: >- +Consul API Gateway Gateway +--- + +# Gateway + +This topic provides full details about the `Gateway` resource. + +## Introduction + +A `Gateway` is an instance of network infrastructure that determines how service traffic should be handled. A `Gateway` contains one or more `listeners` that bind to a set of IP addresses. An `HTTPRoute` or `TCPRoute` can then attach to a gateway listener to direct traffic from the gateway to a service. + +Gateway instances derive their configurations from the `GatewayClass` resource, which acts as a template for individual `Gateway` deployments. Refer to [GatewayClass](/docs/api-gateway/configuration/gatewayclass) for additional information. + +Specify the following parameters to declare a Gateway: + +| Parameter | Description | Required | +|:-----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| +| kind | Specifies the type of configuration object. The value should always be Gateway | Required | +| description | Human-readable string for describing the purpose of the Gateway. | Optional | +| version | Specifies the Kubernetes API version. The value should always be gateway.networking.k8s.io/v1alpha2 | Required | +| scope | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | +| fields | Specifies the configurations for the Gateway. The fields are listed in the Configuration model. Details for each field are described in the Specification. | Required | + + + +## Configuration model + +* gatewayClassName: string | required +* listeners: array of objects | required + * allowedRoutes: object | required + * namespaces: object | required + * from: string | required + * selector: object | required if from is configured to selector + * matchExpressions: array of objects | required if matchLabels is not configured + * key: string | required if matchExpressions is declared + * operator: string | required if matchExpressions is declared + * values: array of strings | required if matchExpressions is declared + * matchLabels: map of strings | required if matchExpressions is not configured + * hostname: string | required + * name: string | required + * port: integer | required + * protocol: string | required + * tls: object | required if protocol is set to HTTPS + * certificateRefs: array or objects | required if tls is declared + * name: string | required if certificateRefs is declared + * namespace: string | required if certificateRefs is declared + * mode: string | required if certificateRefs is declared + * options: map of strings | optional + +## Specification + +This topic provides details about the configuration parameters + +### gatewayClassName +Specifies the name of the `GatewayClass` resource used for the `Gateway` instance. +* Type: string +* Required: required + +### listeners +Specifies the `listeners` associated with the `Gateway`. At least one `listener` must be specified. Each `listener` within a `Gateway` must have a unique combination of `hostname`, `port`, and `protocol`. + +* Type: array of objects +* Required: required + +### listeners.allowedRoutes +Specifies a `namespace` object that defines the types of routes that may be attached to a listener. +* Type: object +* Required: required + +### listeners.allowedRoutes.namespaces +Determines which routes are allowed to attach to the `listener`. Only routes in the same namespace as the `Gateway` may be attached by default. + +### listeners.allowedRoutes.namespaces.from +Specifies the policy for which namespaces a route may attach to a `Gateway` from. Defaults to `Same`. + +This parameter has the following properties: +* Type: string +* Required: required + +You can specify one of the following strings: +* All: Routes in all namespaces may be attached to the Gateway. +* Same: Only routes in the same namespace as the Gateway may be attached. +* Selector: Only routes in namespaces that match the selector configuration may be attached. + +### listeners.allowedRoutes.namespaces.selector +The `selector` configuration matches zero or more namespaces that determine which routes are allowed to attach to the listener. +* Type: Object +* Required: Required when from is configured to selector. + +The selector configuration contains one of the following objects: +* matchExpressions +* matchLabels + +### listeners.allowedRoutes.namespaces.selector.matchExpressions +Specifies an array of requirements for matching namespaces. If a match is found, then routes from the matching namespace(s) are allowed to attach to the `Gateway`. The following table describes members of the `matchExpressions` array: + +| Requirement | Description | Type | +|:-----------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------:| +| key | Specifies that label that the key applies to. | string | +| operator | Specifies the key's relation to a set of values. The following values are valid:In: description of what this means NotIn: description of what this means Exists: description of what this means DoesNotExist: description of what this means | string | +| values | Specifies an array of string values. If the operator is configured to In or NotIn,the values array must be non-empty. If theoperator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. | array of strings | +| scope | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | +| fields | Specifies the configurations for the Gateway. The fields are listed in the Configuration model. Details for each field are described in the Specification. | Required | + +```yaml +namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: + - foo + - bar +``` + +### listeners.allowedRoutes.namespaces.selector.matchLabels +Specifies an array of labels and label values. If a match is found, then routes with the matching label(s) are allowed to attach to the `Gateway`. This selector can contain any arbitrary key/value pair. + +```yaml +namespaceSelector: + matchLabels: + foo: bar +``` + +For more on labels, see [Labels and Selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) + +### listeners.hostname +Specifies the `listeners` hostname +* Type: string +* Required: required + +### listeners.name +Specifies the `listeners` name +* Type: string +* Required: required + +### listeners.port +Specifies the port number that the `listener` will attach to +* Type: integer +* Required: required + +### listeners.protocol +Specifies the protocol the `listener` will use +* Type: string +* Required: required + +Allowed values `TCP`, `HTTP`, `HTTPS` + +### listeners.tls +* Type: Object +* Required: required if protocol is set to HTTPS + +### listeners.tls.certificateRefs +`CertificateRefs` contains a series of references to Kubernetes objects that contains TLS certificates and private keys. These certificates are used to establish a TLS handshake for requests that match the hostname of the associated `listener`. Each reference must be a Kubernetes Secret, and, if using a Secret in a namespace other than the`Gateway`'s, must have a corresponding `ReferencePolicy` created. +* Type: Object or Array +* Required: required if tls is set + +### listeners.tls.mode +* Type: String +* Required: required if certificateRefs is set + +### listeners.tls.options +* Type: Map of Strings +* Required: optional + +## Complete configuration +The following example shows a fully configured Gateway. + +```yaml +kind: Gateway +Description: This gateway enables traffic from A to B. +version: gateway.networking.k8s.io/v1alpha2 +scope: Namespaced +fields: + - name: addresses + supported: false + - name: gatewayClassName + type: string + description: Name of a GatewayClass resource used for this Gateway. + - name: listeners + type: array + description: | + Description of the listeners associated with this Gateway. + items: + fields: + - name: allowedRoutes + type: object + description: | + AllowedRoutes defines the types of routes that + MAY be attached to a Listener and the trusted namespaces where + those Route resources MAY be present. + fields: + - name: kinds + supported: false + - name: namespaces + type: object + description: | + Description of namespaces from which routes may be attached to this Listener. This is restricted + to the namespace of this Gateway by default. + fields: + - name: from + type: string + default: Same + description: | + From indicates where Routes will be selected + for this Gateway." + enum: ["All", "Selector", "Same"] + - name: selector + type: object + description: "Selector must be specified when From is + set to \"Selector\". In that case, only Routes in + Namespaces matching this Selector will be selected + by this Gateway." + fields: + - name: matchExpressions + type: array + description: | + matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + fields: + - name: key + type: string + description: | + key is the label key that the + selector applies to. + - name: operator + type: string + description: | + operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + - name: values + type: array + description: | + values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + - name: matchLabels + type: map + description: | + matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + - name: hostname + type: string + description: | + Hostname specifies the virtual hostname to match for HTTP or HTTPS-based listeners. When unspecified, + all hostnames are matched. This is implemented by checking the HTTP Host header sent on a client request. + - name: name + type: string + description: "Name is the name of the Listener. This name MUST be unique within a Gateway." + - name: port + type: integer + description: "Port is the network port of a listener." + - name: protocol + type: string + description: "Protocol specifies the network protocol this listener expects to receive." + enum: ["HTTP", "HTTPS", "TCP"] + - name: tls + type: object + description: | + TLS is the TLS configuration for the Listener. + This field is required if the Protocol field is "HTTPS". + It is invalid to set this field if the Protocol + field is "HTTP" or "TCP". + fields: + - name: certificateRefs + type: array + description: | + CertificateRefs contains a series of references + to Kubernetes objects that contains TLS certificates and + private keys. These certificates are used to establish + a TLS handshake for requests that match the hostname of + the associated listener. Each reference must be a Kubernetes + Secret, and, if using a Secret in a namespace other than the + Gateway's, must have a corresponding ReferencePolicy created. + items: + fields: + - name: group + supported: false + - name: kind + supported: false + - name: name + type: string + description: Name is the name of the Kubernetes Secret. + - name: namespace + type: string + description: | + Namespace is the namespace of the Secret. When unspecified, the local namespace is inferred. + + Note that when a namespace is specified, a ReferencePolicy + object is required in the specified namespace to + allow that namespace's owner to accept the reference. + - name: mode + type: string + default: Terminate + description: "Mode defines the TLS behavior for the TLS session initiated by the client. The only supported mode at this time is `Terminate`" + enum: ["Terminate"] + - name: options + type: map + description: | + Options are a list of key/value pairs to enable + extended TLS configuration for each implementation. + enum: ["api-gateway.consul.hashicorp.com/tls_min_version","api-gateway.consul.hashicorp.com/tls_max_version","api-gateway.consul.hashicorp.com/tls_cipher_suites",] +``` + diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx new file mode 100644 index 0000000000..2a80fb683d --- /dev/null +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -0,0 +1,37 @@ +--- +layout: docs +page_title: Consul API Gateway GatewayClass +description: >- +Consul API Gateway GatewayClass +--- + +# GatewayClass + +The `GatewayClass` resource is used as a template for creating `Gateway` resources. +The specification includes the name of the controller (`controllerName`) and an API object containing controller-specific configuration resources within the cluster (`parametersRef`). +The value of the `controllerName` field must be set to `hashicorp.com/consul-api-gateway-controller`. + +When gateways are created from a `GatewayClass`, they use the parameters specified in the `GatewayClass` at the time of instantiation. + +Add the `kind: GatewayClass` option to the the gateway values file to declare a gateway class. +The following example creates a gateway class called `test-gateway-class`: + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GatewayClass + metadata: + name: test-gateway-class + spec: + controllerName: 'hashicorp.com/consul-api-gateway-controller' + parametersRef: + group: api-gateway.consul.hashicorp.com + kind: GatewayClassConfig + name: test-gateway-class-config + ``` + + + +Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass) for details about configuring gateway classes. + diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx new file mode 100644 index 0000000000..7648b04736 --- /dev/null +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -0,0 +1,57 @@ +--- +layout: docs +page_title: Consul API Gateway GatewayClassConfig +description: >- +Consul API Gateway GatewayClassConfig +--- + +# GatewayClassConfig + +The `GatewayClassConfig` object describes Consul API Gateway-related configuration parameters for the [`GatewayClass`](#gatewayclass). + +Add the `kind: GatewayClassConfig` option to the gateway values file to declare a gateway class. +The following example creates a gateway class configuration called `test-gateway-class-config`: + + + + ```yaml + apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 + kind: GatewayClassConfig + metadata: + name: test-gateway-class-config + spec: + useHostPorts: true + logLevel: 'trace' + consul: + scheme: 'https' + ports: + http: 8501 + grpc: 8502 + ``` + + + +The following table describes the allowed parameters for the `spec` array: + +| Parameter | Description | Type | Default | +| --- | --- | ---- | ------- | +| `consul.address` | Specifies the address of the Consul server to communicate with in the gateway pod. If unspecified, the pod will attempt to use a local agent on the host on which the pod is running. | String | N/A | +| `consul.authentication.account` | Specifies the Kubernetes service account to use for authentication. | String | N/A | +| `consul.authentication.managed` | Set to `true` to enable deployments to run with managed service accounts created by the gateway controller. The `consul.authentication.account` field is ignored when this option is enabled. | Boolean | `false` | +| `consul.authentication.method` | Specifies the Consul auth method used for initial authentication by Consul API Gateway. | String | N/A | +| `consul.authentication.namespace` | Specifies the Consul namespace to use for authentication. | String | N/A | +| `consul.ports.grpc` | Specifies the gRPC port for Consul's xDS server. | Integer | `8502` | +| `consul.ports.http` | Specifies the port for Consul's HTTP server. | Integer | `8500` | +| `consul.scheme` | Specifies the scheme to use for connecting to Consul. The supported values are `"http"` and `"https"`. | String | `"http"` | +| `copyAnnotations.service` | List of annotations to copy to the gateway service. | Array | `["external-dns.alpha.kubernetes.io/hostname"]` | +| `deployment.defaultInstances` | Specifies the number of instances to deploy by default for each gateway. | Integer | 1 | +| `deployment.maxInstances` | Specifies the maximum allowed number of instances per gateway. | Integer | 8 | +| `deployment.minInstances` | Specifies the minimum allowed number of instances per gateway. | Integer | 1 | +| `image.consulAPIGateway` | The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | +| `image.envoy` | Specifies the container image to use for Envoy. View available image tags on [DockerHub](https://hub.docker.com/r/envoyproxy/envoy/tags). | String | `"envoyproxy/envoy:RELEASE_VERSION"` | +| `logLevel` | Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. | String | `"info"` | +| `nodeSelector` | Specifies a set of parameters that constrain the nodes on which the pod can run. Defining nodes with the `nodeSelector` enables the pod to fit on a node. The selector must match a node's labels for the pod to be scheduled on that node. Refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) for additional information. | Object | N/A | +| `serviceType` | Specifies the ingress methods for a service. The following values are supported:
`ClusterIP`
`NodePort`
`LoadBalancer`. | String | N/A | +| `useHostPorts` | If set to `true`, then the Envoy container ports are mapped to host ports. | Boolean | `false` | + +Refer to the [Consul API Gateway repository](https://github.com/hashicorp/consul-api-gateway/blob/main/config/crd/bases/api-gateway.consul.hashicorp.com_gatewayclassconfigs.yaml) for the complete specification. diff --git a/website/content/docs/api-gateway/configuration/index.mdx b/website/content/docs/api-gateway/configuration/index.mdx new file mode 100644 index 0000000000..ebdaaeb6fa --- /dev/null +++ b/website/content/docs/api-gateway/configuration/index.mdx @@ -0,0 +1,17 @@ +--- +layout: docs +page_title: Consul API Gateway Configuration +description: >- +Consul API Gateway Configuration +--- + +# Configuration + +This topic provides an overview of the configuration items that enable Consul API Gateway to facilitate ingress into your Consul service mesh. + +- [Gateway](/docs/api-gateway/configuration/gateway): Defines the main infrastructure resource that links API gateway components. It specifies the name of the `GatewayClass` and one or more listeners (see [Listeners](/docs/api-gateway/configuration/gateway#listeners)), which specify the logical endpoints bound to the gateway's addresses. Refer to Configuration > Gateway for details on configuration. +- [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig): Describes additional Consul API Gateway-related configuration parameters for the GatewayClass resource. +- [GatewayClass](/docs/api-gateway/configuration/gatewayclass): Defines a class of gateway resources that you can use as a template for creating gateways. +- [Routes](/docs/api-gateway/configuration/routes): Specifies the path from the gateway to the backend service(s)client to the listener. + + diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx new file mode 100644 index 0000000000..987e8193ab --- /dev/null +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -0,0 +1,89 @@ +--- +layout: docs +page_title: Consul API Gateway Routes +description: >- +Consul API Gateway Routes +--- + +# Route + +Routes are independent configuration objects that are associated with specific listeners. + +Declare a route with either `kind: HTTPRoute` or `kind: TCPRoute` and configure the route parameters in the `spec` block. +Refer to the Kubernetes Gateway API documentation for each object type for details: + +- [HTTPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute) +- [TCPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute) + +The following example creates a route named `example-route` associated with a listener defined in `example-gateway`. + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: HTTPRoute + metadata: + name: example-route + spec: + parentRefs: + - name: example-gateway + rules: + - backendRefs: + - kind: Service + name: echo + port: 8080 + ``` + + + +To create a route for a `backendRef` in a different namespace, you must also +create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy). + +The following example creates a route named `example-route` in namespace `gateway-namespace`. This route has a `backendRef` in namespace `service-namespace`. Traffic is allowed because the `ReferencePolicy`, named `reference-policy` in namespace `service-namespace`, allows traffic from `HTTPRoutes` in `gateway-namespace` to `Services` in `service-namespace`. + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: HTTPRoute + metadata: + name: example-route + namespace: gateway-namespace + spec: + parentRefs: + - name: example-gateway + rules: + - backendRefs: + - kind: Service + name: echo + namespace: service-namespace + port: 8080 + --- + + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: ReferencePolicy + metadata: + name: reference-policy + namespace: service-namespace + spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: gateway-namespace + to: + - group: "" + kind: Service + name: echo + ``` + + + +## MeshService + +The `MeshService` configuration holds a reference to an externally-managed Consul service mesh service and can be used as a `backendRef` for a [`Route`](#route). + +| Parameter | Description | Type | Default | +| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------------- | +| `name` | Specifies the service name for a Consul service. It is assumed this service will exist in either the `consulDestinationNamespace` or mirrored Consul namespace from where this custom resource is defined, depending on the Helm configuration. + +Refer to the [Consul API Gateway repository](https://github.com/hashicorp/consul-api-gateway/blob/main/config/crd/bases/api-gateway.consul.hashicorp.com_meshservices.yaml) for the complete specification. diff --git a/website/content/docs/api-gateway/consul-api-gateway-install.mdx b/website/content/docs/api-gateway/consul-api-gateway-install.mdx index fb48565baa..6c62f6719e 100644 --- a/website/content/docs/api-gateway/consul-api-gateway-install.mdx +++ b/website/content/docs/api-gateway/consul-api-gateway-install.mdx @@ -11,7 +11,64 @@ This topic describes how to use the Consul API Gateway add-on module. It include ## Requirements -Ensure that the environment you are deploying Consul API Gateway in meets the requirements listed in the [Technical Specifications][tech-specs]. This includes validating that the requirements for minimum versions of software are met. See the [Release Notes][rel-notes] for the version of API Gateway you are deploying. +Verify that your environment meets the following requirements prior to using Consul API Gateway. + +### Datacenter Requirements + +Your datacenter must meet the following requirements prior to configuring the Consul API Gateway: + +- Kubernetes 1.21+ +- Kubernetes 1.24 is not supported at this time. +- `kubectl` 1.21+ +- Consul 1.11.2+ +- HashiCorp Consul Helm chart 0.45.0+ +- Consul Service Mesh must be deployed on the Kubernetes cluster that API Gateway is deployed on. +- Envoy: Envoy proxy support is determined by the Consul version deployed. Refer to [Envoy Integration](/docs/connect/proxies/envoy) for details. + +### TCP Port Requirements + +The following table describes the TCP port requirements for each component of the API Gateway. + +| Port | Description | Component | +| ---- | ----------- | --------- | +| 9090 | Secret discovery service (SDS) | Gateway controller pod
Gateway instance pod | +| 20000 | Kubernetes readiness probe | Gateway instance pod | +| Configurable | Port for scraping Prometheus metrics. Disabled by default. | Gateway controller pod | + +## Consul Server Deployments + +- Consul Editions supported: OSS and Enterprise +- Supported Consul Server deployment types: +- Self-Managed +- HCP Consul + +## Deployment Environments + +Consul API Gateway can be deployed in the following Kubernetes-based environments: + +- Generic Kubernetes +- AWS Elastic Kubernetes Service (EKS) +- Google Kubernetes Engine (GKE) +- Azure Kubernetes Service (AKS) + +## Kubernetes Gateway API Specification - Supported Versions + +See the Release Notes for the version of Consul API Gateway being used. + +## Resource Allocations + +The following resources are allocated for each component of the API Gateway. + +### Gateway Controller Pod + +- **CPU**: None. Either the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. +- **Memory**: None. Either the the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. + +### Gateway Instance Pod + +- **CPU**: None. Either the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. +- **Memory**: None. Either the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. + ## Installation @@ -47,379 +104,9 @@ Ensure that the environment you are deploying Consul API Gateway in meets the re $ helm install consul hashicorp/consul --version 0.45.0 --values values.yaml --create-namespace --namespace consul ``` -## Usage - -1. Verify that the [requirements](#requirements) have been met. -1. Verify that the Consul API Gateway CRDs and controller have been installed and applied (see [Installation](#installation)). -1. Configure the artifacts described below in [Configuration](#configuration). - - - - ```yaml - apiGateway: - managedGatewayClass: - enabled: true - ``` - - - -1. Issue the `kubectl apply` command to implement the configurations, e.g.: - - ```shell-session - $ kubectl apply -f gateway.yaml routes.yaml - ``` - - - -## Configuration - -Configure the following artifacts to facilitate ingress into your Consul service mesh: - -- [GatewayClassConfig](#gatewayclassconfig): Describes additional Consul API Gateway-related configuration parameters for the `GatewayClass` resource. -- [GatewayClass](#gatewayclass): Defines a class of gateway resources that you can use as a template for creating gateways. -- [Gateway](#gateway): Defines the main infrastructure resource that links API gateway components. It specifies the name of the `GatewayClass` and one or more `listeners` (see [Listeners](#listeners)), which specify the logical endpoints bound to the gateway's addresses. -- [Routes](#routes): Specifies the path from the client to the listener. - --> **Note:** Add the following `managedGatewayClass` configuration to the `values.yaml` Helm configuration to enable the `GatewayClassConfig` and `GatewayClass` to be created automatically. The gateway, listeners, and routes will need to be configured manually. When `managedGatewayClass` is enabled, the [`serviceType`](/docs/k8s/helm#v-apigateway-managedgatewayclass-servicetype) for a managed `GatewayClass` will also default to `LoadBalancer`, which is appropriate for most deployments to managed Kubernetes cloud offerings (i.e., EKS, GKE, AKS). Other deployments, such as to a [kind](https://kind.sigs.k8s.io/) cluster, may require specifying `NodePort` or `ClusterIP`, instead. - -### GatewayClassConfig - -The `GatewayClassConfig` object describes Consul API Gateway-related configuration parameters for the [`GatewayClass`](#gatewayclass). - -Add the `kind: GatewayClassConfig` option to the gateway values file to declare a gateway class. -The following example creates a gateway class configuration called `test-gateway-class-config`: - - - -```yaml -apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 -kind: GatewayClassConfig -metadata: - name: test-gateway-class-config -spec: - useHostPorts: true - logLevel: 'trace' - consul: - scheme: 'https' - ports: - http: 8501 - grpc: 8502 -``` - - - -The following table describes the allowed parameters for the `spec` array: - -| Parameter | Description | Type | Default | -| --- | --- | ---- | ------- | -| `consul.address` | Specifies the address of the Consul server to communicate with in the gateway pod. If unspecified, the pod will attempt to use a local agent on the host on which the pod is running. | String | N/A | -| `consul.authentication.account` | Specifies the Kubernetes service account to use for authentication. | String | N/A | -| `consul.authentication.managed` | Set to `true` to enable deployments to run with managed service accounts created by the gateway controller. The `consul.authentication.account` field is ignored when this option is enabled. | Boolean | `false` | -| `consul.authentication.method` | Specifies the Consul auth method used for initial authentication by Consul API Gateway. | String | N/A | -| `consul.authentication.namespace` | Specifies the Consul namespace to use for authentication. | String | N/A | -| `consul.ports.grpc` | Specifies the gRPC port for Consul's xDS server. | Integer | `8502` | -| `consul.ports.http` | Specifies the port for Consul's HTTP server. | Integer | `8500` | -| `consul.scheme` | Specifies the scheme to use for connecting to Consul. The supported values are `"http"` and `"https"`. | String | `"http"` | -| `copyAnnotations.service` | List of annotations to copy to the gateway service. | Array | `["external-dns.alpha.kubernetes.io/hostname"]` | -| `deployment.defaultInstances` | Specifies the number of instances to deploy by default for each gateway. | Integer | 1 | -| `deployment.maxInstances` | Specifies the maximum allowed number of instances per gateway. | Integer | 8 | -| `deployment.minInstances` | Specifies the minimum allowed number of instances per gateway. | Integer | 1 | -| `image.consulAPIGateway` | The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | -| `image.envoy` | Specifies the container image to use for Envoy. View available image tags on [DockerHub](https://hub.docker.com/r/envoyproxy/envoy/tags). | String | `"envoyproxy/envoy:RELEASE_VERSION"` | -| `logLevel` | Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. | String | `"info"` | -| `nodeSelector` | Specifies a set of parameters that constrain the nodes on which the pod can run. Defining nodes with the `nodeSelector` enables the pod to fit on a node. The selector must match a node's labels for the pod to be scheduled on that node. Refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) for additional information. | Object | N/A | -| `serviceType` | Specifies the ingress methods for a service. The following values are supported:
`ClusterIP`
`NodePort`
`LoadBalancer`. | String | N/A | -| `useHostPorts` | If set to `true`, then the Envoy container ports are mapped to host ports. | Boolean | `false` | - -Refer to the [Consul API Gateway repository](https://github.com/hashicorp/consul-api-gateway/blob/main/config/crd/bases/api-gateway.consul.hashicorp.com_gatewayclassconfigs.yaml) for the complete specification. - -### GatewayClass - -The `GatewayClass` resource is used as a template for creating `Gateway` resources. -The specification includes the name of the controller (`controllerName`) and an API object containing controller-specific configuration resources within the cluster (`parametersRef`). -The value of the `controllerName` field must be set to `hashicorp.com/consul-api-gateway-controller`. - -When gateways are created from a `GatewayClass`, they use the parameters specified in the `GatewayClass` at the time of instantiation. - -Add the `kind: GatewayClass` option to the the gateway values file to declare a gateway class. -The following example creates a gateway class called `test-gateway-class`: - - - -```yaml -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: GatewayClass -metadata: - name: test-gateway-class -spec: - controllerName: 'hashicorp.com/consul-api-gateway-controller' - parametersRef: - group: api-gateway.consul.hashicorp.com - kind: GatewayClassConfig - name: test-gateway-class-config -``` - - - -Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayClass) for details about configuring gateway classes. - -### Gateway - -The gateway configuration is the main infrastructure resource that links API gateway components. It specifies the name of the `GatewayClass` and one or more `listeners`. - -Add the `kind: Gateway` option to the configuration file to declare a gateway. -The following example creates a gateway called `example-gateway`. -The gateway is based on the `test-gateway-class` and includes a listener called `https` (see [Listeners](#listeners) for details about the `listener` configuration). - - - -```yaml -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: Gateway -metadata: - name: example-gateway - annotations: - 'external-dns.alpha.kubernetes.io/hostname': DNS_HOSTNAME -spec: - gatewayClassName: test-gateway-class - listeners: - - protocol: HTTPS - hostname: DNS_HOSTNAME - port: 443 - name: https - allowedRoutes: - namespaces: - from: Same - tls: - certificateRefs: - - name: gateway-production-certificate -``` - - - -For a listener's `certificateRef` to reference a secret in a different namespace, you must also create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy). - -The following example creates a `Gateway` named `example-gateway` in `gateway-namespace`. This `Gateway` has a `certificateRef` in `secret-namespace`. -The reference is allowed because `reference-policy` in `secret-namespace` lets `Gateways` in `gateway-namespace` reference `Secrets` in `secret-namespace`. - - - -```yaml -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: Gateway -metadata: - name: example-gateway - namespace: gateway-namespace - annotations: - 'external-dns.alpha.kubernetes.io/hostname': DNS_HOSTNAME -spec: - gatewayClassName: test-gateway-class - listeners: - - protocol: HTTPS - hostname: DNS_HOSTNAME - port: 443 - name: https - allowedRoutes: - namespaces: - from: Same - tls: - certificateRefs: - - name: gateway-production-certificate - namespace: secret-namespace ---- - -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: ReferencePolicy -metadata: - name: reference-policy - namespace: secret-namespace -spec: - from: - - group: gateway.networking.k8s.io - kind: Gateway - namespace: gateway-namespace - to: - - group: "" - kind: Secret - name: gateway-production-certificate -``` - - - -Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Gateway) for further details about configuring gateways. - -#### Listeners - -Listeners are the logical endpoints bound to the gateway's addresses. -Add the `listener` object to the `gateway` configuration and specify the following properties to define a listener: - -| Parameter | Description | Type | Default | -| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------------- | -| `hostname` | Specifies the virtual hostname to match for protocol types. | String | none | -| `port` | Specifies the network port number. | Integer | none | -| `protocol` | Specifies the network protocol expected by the listener. | String | `http` | -| `tls` | Collection of parameters that specify TLS options for the listener. Refer to the [`GatewayTLSConfig`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.GatewayTLSConfig) documentation for additional information about configuring TLS. | Object | N/A | -| `tls.mode` | Specifies a mode for operating Consul API Gateway listeners over TLS.
You can only specify the `Terminate` mode, which configures the TLS session between the downstream client and the gateway to terminate at the gateway.
Refer to the [`TLSModeType` documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TLSModeType) for additional information. | String | `Terminate` | -| `tls.certificateRefs` | Specifies the name of secret object used for Envoy SDS (Secret Discovery Service) to support terminating TLS. Refer to the [`[]*SecretObjectReference` documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.SecretObjectReference) for additional information. | String | N/A | -| `tls.options` | Specifies key/value pairs to enable extended TLS configuration specific to an implementation. | Object | N/A | -| `tls.options.tls_min_version` | Specifies the minimum TLS version supported for the listener. The following values are supported: `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, `TLSv1_3`. | String | `TLS 1.2` | -| `tls.options.tls_max_version` | Specifies the maximum TLS version supported for the listener. The specified version must be greater than or equal to `TLSMinVersion`. The following values are supported: `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, `TLSv1_3`. | String | `TLS 1.3` | -| `tls.options.tls_cipher_suites` | Specifies the list of TLS cipher suites to support when negotiating connections using TLS 1.2 or earlier.
If unspecified, a [more secure set of cipher suites](https://github.com/hashicorp/consul-api-gateway/blob/main/internal/common/tls.go#L3-L10) than Envoy's current [default server cipher list](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport_sockets/tls/v3/common.proto#envoy-v3-api-field-extensions-transport-sockets-tls-v3-tlsparameters-cipher-suites) will be used.
The full list of supported cipher suites can seen in [`internal/common/tls.go`](https://github.com/hashicorp/consul-api-gateway/blob/main/internal/common/tls.go) and is dependent on underlying support in Envoy. | String | See description | - -Refer to the [Kubernetes Gateway API documentation](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Listener) for details about configuring listeners. - -#### Scaling - -You can scale a logical gateway object to multiple instances with the [`kubectl scale`](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#scaling-a-deployment) command. The object scales according to the bounds set in `GatewayClassConfig`. - -```shell-session -$ kubectl get deployment --selector api-gateway.consul.hashicorp.com/name=example-gateway -NAME READY UP-TO-DATE AVAILABLE -example-gateway 1/1 1 1 -``` -```shell-session -$ kubectl scale deployment/example-gateway --replicas=3 -deployment.apps/example-gateway scaled -``` -```shell-session -$ kubectl get deployment --selector api-gateway.consul.hashicorp.com/name=example-gateway -NAME READY UP-TO-DATE AVAILABLE -example-gateway 3/3 3 3 -``` - -### Route - -Routes are independent configuration objects that are associated with specific listeners. - -Declare a route with either `kind: HTTPRoute` or `kind: TCPRoute` and configure the route parameters in the `spec` block. -Refer to the Kubernetes Gateway API documentation for each object type for details: - -- [HTTPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute) -- [TCPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute) - -The following example creates a route named `example-route` associated with a listener defined in `example-gateway`. - - - -```yaml -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: HTTPRoute -metadata: - name: example-route -spec: - parentRefs: - - name: example-gateway - rules: - - backendRefs: - - kind: Service - name: echo - port: 8080 -``` - - - -To create a route for a `backendRef` in a different namespace, you must also -create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy). - -The following example creates a route named `example-route` in namespace `gateway-namespace`. This route has a `backendRef` in namespace `service-namespace`. Traffic is allowed because the `ReferencePolicy`, named `reference-policy` in namespace `service-namespace`, allows traffic from `HTTPRoutes` in `gateway-namespace` to `Services` in `service-namespace`. - - - -```yaml -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: HTTPRoute -metadata: - name: example-route - namespace: gateway-namespace -spec: - parentRefs: - - name: example-gateway - rules: - - backendRefs: - - kind: Service - name: echo - namespace: service-namespace - port: 8080 ---- - -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: ReferencePolicy -metadata: - name: reference-policy - namespace: service-namespace -spec: - from: - - group: gateway.networking.k8s.io - kind: HTTPRoute - namespace: gateway-namespace - to: - - group: "" - kind: Service - name: echo -``` - - - -### MeshService - -The `MeshService` configuration holds a reference to an externally-managed Consul service mesh service and can be used as a `backendRef` for a [`Route`](#route). - -| Parameter | Description | Type | Default | -| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------------- | -| `name` | Specifies the service name for a Consul service. It is assumed this service will exist in either the `consulDestinationNamespace` or mirrored Consul namespace from where this custom resource is defined, depending on the Helm configuration. - -Refer to the [Consul API Gateway repository](https://github.com/hashicorp/consul-api-gateway/blob/main/config/crd/bases/api-gateway.consul.hashicorp.com_meshservices.yaml) for the complete specification. -[tech-specs]: /docs/api-gateway/tech-specs [rel-notes]: /docs/release-notes diff --git a/website/content/docs/api-gateway/index.mdx b/website/content/docs/api-gateway/index.mdx index a5bfd9dc8e..8ec1c0f5a6 100644 --- a/website/content/docs/api-gateway/index.mdx +++ b/website/content/docs/api-gateway/index.mdx @@ -18,7 +18,7 @@ Consul API Gateway solves the following primary use cases: - **Controlling access at the point of entry**: Consul API Gateway allows users to set the protocols of external connection requests and provide clients with TLS certificates from trusted providers (e.g., Verisign, Let’s Encrypt). - **Simplifying traffic management**: The Consul API Gateway can load balance requests across services and route traffic to the appropriate service by matching one or more criteria, such as hostname, path, header presence or value, and HTTP Method type (e.g., GET, POST, PATCH). -## Implementation +## How Does Consul API Gateway Work? Consul API Gateway can be deployed on Kubernetes-based runtime environments and requires that Consul service mesh be deployed on the Kubernetes cluster. diff --git a/website/content/docs/api-gateway/tech-specs.mdx b/website/content/docs/api-gateway/tech-specs.mdx deleted file mode 100644 index e499349198..0000000000 --- a/website/content/docs/api-gateway/tech-specs.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -layout: docs -page_title: Consul API Gateway Technical Specifications -description: >- - This topic describes technical specifications for Consul API Gateway. ---- - -# Technical Specifications - -This topic describes the technical specifications associated with using Consul API Gateway. - -## Requirements - -Verify that your environment meets the following requirements prior to using Consul API Gateway. - -### Datacenter Requirements - -Your datacenter must meet the following requirements prior to configuring the Consul API Gateway: - -- Kubernetes 1.21+ - - Kubernetes 1.24 is not supported at this time. -- `kubectl` 1.21+ -- Consul 1.11.2+ -- HashiCorp Consul Helm chart 0.45.0+ -- Consul Service Mesh must be deployed on the Kubernetes cluster that API Gateway is deployed on. -- Envoy: Envoy proxy support is determined by the Consul version deployed. Refer to [Envoy Integration](/docs/connect/proxies/envoy) for details. - -### TCP Port Requirements - -The following table describes the TCP port requirements for each component of the API Gateway. - -| Port | Description | Component | -| ---- | ----------- | --------- | -| 9090 | Secret discovery service (SDS) | Gateway controller pod
Gateway instance pod | -| 20000 | Kubernetes readiness probe | Gateway instance pod | -| Configurable | Port for scraping Prometheus metrics. Disabled by default. | Gateway controller pod | - -## Consul Server Deployments - -- Consul Editions supported: OSS and Enterprise -- Supported Consul Server deployment types: - - Self-Managed - - HCP Consul - -## Deployment Environments - -Consul API Gateway can be deployed in the following Kubernetes-based environments: - -- Generic Kubernetes -- AWS Elastic Kubernetes Service (EKS) -- Google Kubernetes Engine (GKE) -- Azure Kubernetes Service (AKS) - -## Kubernetes Gateway API Specification - Supported Versions - -See the Release Notes for the version of Consul API Gateway being used. - -## Resource Allocations - -The following resources are allocated for each component of the API Gateway. - -### Gateway Controller Pod - -- **CPU**: None. Either the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. -- **Memory**: None. Either the the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. - -### Gateway Instance Pod - -- **CPU**: None. Either the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. -- **Memory**: None. Either the namespace or cluster default is allocated, depending on the Kubernetes cluster configuration. diff --git a/website/content/docs/api-gateway/upgrade-specific-versions.mdx b/website/content/docs/api-gateway/upgrades.mdx similarity index 100% rename from website/content/docs/api-gateway/upgrade-specific-versions.mdx rename to website/content/docs/api-gateway/upgrades.mdx diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx new file mode 100644 index 0000000000..1eae64c4e5 --- /dev/null +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -0,0 +1,131 @@ +--- +layout: docs +page_title: Consul API Gateway Basic Usage +description: >- +Consul API Gateway Basic Usage +--- + + +# Basic Usage + +1. Verify that the [requirements](/docs/api-gateway/consul-api-gateway-install#requirments) have been met. +1. Verify that the Consul API Gateway CRDs and controller have been installed and applied (see [Installation](/docs/api-gateway/consul-api-gateway-install)). +1. Configure the artifacts described below in [Configuration](/docs/api-gateway/configuration). + + + + ```yaml + apiGateway: + managedGatewayClass: + enabled: true + ``` + + + +1. Issue the `kubectl apply` command to implement the configurations, e.g.: + +```shell-session +$ kubectl apply -f gateway.yaml routes.yaml + ``` + + + +## Common Error Messages + +Some of the errors messages commonly encountered during installation and operations of Consul API Gateway are listed below, along with suggested methods for resolving them. + +If the error message is not listed on this page, it may be listed on the main [Consul Common errors][consul-common-errors] page. If the error message is not listed on that page either, please consider following our general [Troubleshooting Guide][troubleshooting] or reach out to us in [Discuss](https://discuss.hashicorp.com/). + + + +### Helm installation failed: "no matches for kind" + +``` +Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: [unable to recognize "": no matches for kind "GatewayClass" in version "gateway.networking.k8s.io/v1alpha2", unable to recognize "": no matches for kind "GatewayClassConfig" in version "api-gateway.consul.hashicorp.com/v1alpha1"] +``` +**Conditions:** +When this error occurs during the process of installing Consul API Gateway, it is usually caused by not having the required CRD files installed in Kubernetes prior to installing Consul API Gateway. + +**Impact:** +The installation process will typically fail after this error message is generated + +**Recommended Action:** +Install the required CRDs by using the command in Step 1 of the [Consul API Gateway installation instructions](/docs/api-gateway/consul-api-gateway-install) and then retry installing Consul API Gateway. + diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 3785b41823..2e312545b1 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -385,17 +385,43 @@ "title": "Installation", "path": "api-gateway/consul-api-gateway-install" }, - { - "title": "Technical Specifications", - "path": "api-gateway/tech-specs" - }, - { - "title": "Common Errors", - "path": "api-gateway/common-errors" - }, { "title": "Upgrades", - "path": "api-gateway/upgrade-specific-versions" + "path": "api-gateway/upgrades" + }, + { + "title": "Usage", + "routes": [ + { + "title": "Basic Usage", + "path": "api-gateway/usage/basic-usage" + } + ] + }, + { + "title": "Configuration", + "routes": [ + { + "title": "Overview", + "path": "api-gateway/configuration" + }, + { + "title": "Gateway", + "path": "api-gateway/configuration/gateway" + }, + { + "title": "Gateway Class", + "path": "api-gateway/configuration/gatewayclass" + }, + { + "title": "Gateway Class", + "path": "api-gateway/configuration/gatewayclassconfig" + }, + { + "title": "Gateway Class", + "path": "api-gateway/configuration/routes" + } + ] } ] }, From 96ef69ffb4a1a2fabdea0cbf8514a706c032d626 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Wed, 6 Jul 2022 09:52:58 -0500 Subject: [PATCH 003/339] delete extra file --- .../docs/api-gateway/common-errors.mdx | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 website/content/docs/api-gateway/common-errors.mdx diff --git a/website/content/docs/api-gateway/common-errors.mdx b/website/content/docs/api-gateway/common-errors.mdx deleted file mode 100644 index f49c9fefc5..0000000000 --- a/website/content/docs/api-gateway/common-errors.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -layout: docs -page_title: Common Error Messages ---- - -# Common Error Messages - -Some of the errors messages commonly encountered during installation and operations of Consul API Gateway are listed below, along with suggested methods for resolving them. - -If the error message is not listed on this page, it may be listed on the main [Consul Common errors][consul-common-errors] page. If the error message is not listed on that page either, please consider following our general [Troubleshooting Guide][troubleshooting] or reach out to us in [Discuss](https://discuss.hashicorp.com/). - - - -### Helm installation failed: "no matches for kind" - -``` -Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: [unable to recognize "": no matches for kind "GatewayClass" in version "gateway.networking.k8s.io/v1alpha2", unable to recognize "": no matches for kind "GatewayClassConfig" in version "api-gateway.consul.hashicorp.com/v1alpha1"] -``` -**Conditions:** -When this error occurs during the process of installing Consul API Gateway, it is usually caused by not having the required CRD files installed in Kubernetes prior to installing Consul API Gateway. - -**Impact:** -The installation process will typically fail after this error message is generated - -**Recommended Action:** -Install the required CRDs by using the command in Step 1 of the [Consul API Gateway installation instructions][install-instructions] and then retry installing Consul API Gateway. - - - - - -[consul-common-errors]: /docs/troubleshoot/common-errors -[troubleshooting]: https://learn.hashicorp.com/consul/day-2-operations/advanced-operations/troubleshooting -[install-instructions]: /docs/api-gateway/consul-api-gateway-install#installation \ No newline at end of file From ef36a80ebf6f32dc47b10bb6a358e63952f5509e Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Wed, 6 Jul 2022 11:59:40 -0500 Subject: [PATCH 004/339] fix render issue --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- .../content/docs/api-gateway/configuration/gatewayclass.mdx | 2 +- .../docs/api-gateway/configuration/gatewayclassconfig.mdx | 2 +- website/content/docs/api-gateway/configuration/index.mdx | 2 +- website/content/docs/api-gateway/configuration/routes.mdx | 2 +- website/data/docs-nav-data.json | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index c1dd183268..5300fe7ca9 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway Gateway description: >- -Consul API Gateway Gateway + Consul API Gateway Gateway --- # Gateway diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 2a80fb683d..5f92df12dd 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway GatewayClass description: >- -Consul API Gateway GatewayClass + Consul API Gateway GatewayClass --- # GatewayClass diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 7648b04736..3d660d1a7c 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway GatewayClassConfig description: >- -Consul API Gateway GatewayClassConfig + Consul API Gateway GatewayClassConfig --- # GatewayClassConfig diff --git a/website/content/docs/api-gateway/configuration/index.mdx b/website/content/docs/api-gateway/configuration/index.mdx index ebdaaeb6fa..8952a0dc4a 100644 --- a/website/content/docs/api-gateway/configuration/index.mdx +++ b/website/content/docs/api-gateway/configuration/index.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway Configuration description: >- -Consul API Gateway Configuration + Consul API Gateway Configuration --- # Configuration diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index 987e8193ab..32d314cec1 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway Routes description: >- -Consul API Gateway Routes + Consul API Gateway Routes --- # Route diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index a8ef09cef5..148d54f68c 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -431,11 +431,11 @@ "path": "api-gateway/configuration/gatewayclass" }, { - "title": "Gateway Class", + "title": "Gateway Class Config", "path": "api-gateway/configuration/gatewayclassconfig" }, { - "title": "Gateway Class", + "title": "Routes", "path": "api-gateway/configuration/routes" } ] From 7a8da641c3032776c22d239ec2cd6f2cf74a5b64 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Wed, 6 Jul 2022 15:38:49 -0500 Subject: [PATCH 005/339] fix render issue --- website/content/docs/api-gateway/usage/basic-usage.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index 1eae64c4e5..9333df2130 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway Basic Usage description: >- -Consul API Gateway Basic Usage + Consul API Gateway Basic Usage --- From d9f0a98121b3f4398dedeaf62a5b1b1bced5ebfd Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:21:40 -0500 Subject: [PATCH 006/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 5300fe7ca9..8ff46ef827 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -167,7 +167,7 @@ Allowed values `TCP`, `HTTP`, `HTTPS` * Required: optional ## Complete configuration -The following example shows a fully configured Gateway. +The following example shows a fully configured `Gateway`. ```yaml kind: Gateway From 7f28c388ba29d5343d0ba85114c0bf1febc8e00f Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:21:45 -0500 Subject: [PATCH 007/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 8ff46ef827..e82fb8b462 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -156,7 +156,7 @@ Allowed values `TCP`, `HTTP`, `HTTPS` ### listeners.tls.certificateRefs `CertificateRefs` contains a series of references to Kubernetes objects that contains TLS certificates and private keys. These certificates are used to establish a TLS handshake for requests that match the hostname of the associated `listener`. Each reference must be a Kubernetes Secret, and, if using a Secret in a namespace other than the`Gateway`'s, must have a corresponding `ReferencePolicy` created. * Type: Object or Array -* Required: required if tls is set +* Required: required if `tls` is set ### listeners.tls.mode * Type: String From 446c6dff31bb5ab59d701596bcb5d3b3025c55c3 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:22:26 -0500 Subject: [PATCH 008/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index e82fb8b462..0d9b4cd050 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -53,7 +53,7 @@ Specify the following parameters to declare a Gateway: ## Specification -This topic provides details about the configuration parameters +This topic provides details about the configuration parameters. ### gatewayClassName Specifies the name of the `GatewayClass` resource used for the `Gateway` instance. From bf53a73dde9fae4e4743df8ec9ba29337c346ea8 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:22:39 -0500 Subject: [PATCH 009/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 0d9b4cd050..b217f92903 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -82,9 +82,9 @@ This parameter has the following properties: * Required: required You can specify one of the following strings: -* All: Routes in all namespaces may be attached to the Gateway. -* Same: Only routes in the same namespace as the Gateway may be attached. -* Selector: Only routes in namespaces that match the selector configuration may be attached. +* `All`: Routes in all namespaces may be attached to the `Gateway`. +* `Same`: Only routes in the same namespace as the `Gateway` may be attached. +* `Selector`: Only routes in namespaces that match the `selector` may be attached. ### listeners.allowedRoutes.namespaces.selector The `selector` configuration matches zero or more namespaces that determine which routes are allowed to attach to the listener. From e305fb232db74067ae0338cdaa6fa656b065d20f Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:22:47 -0500 Subject: [PATCH 010/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index b217f92903..4d6e27c3bf 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -133,7 +133,7 @@ Specifies the `listeners` hostname * Required: required ### listeners.name -Specifies the `listeners` name +Specifies the `listener`'s name * Type: string * Required: required From 396a75ff06bc254b5baca0c917f2c8ecd1f1b5fd Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:22:52 -0500 Subject: [PATCH 011/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 4d6e27c3bf..226e368e64 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -151,7 +151,7 @@ Allowed values `TCP`, `HTTP`, `HTTPS` ### listeners.tls * Type: Object -* Required: required if protocol is set to HTTPS +* Required: required if `protocol` is set to `HTTPS` ### listeners.tls.certificateRefs `CertificateRefs` contains a series of references to Kubernetes objects that contains TLS certificates and private keys. These certificates are used to establish a TLS handshake for requests that match the hostname of the associated `listener`. Each reference must be a Kubernetes Secret, and, if using a Secret in a namespace other than the`Gateway`'s, must have a corresponding `ReferencePolicy` created. From 7bb6e8379ae301b4fcc29b3432b05bd4f2f9ae6a Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:23:03 -0500 Subject: [PATCH 012/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 226e368e64..6d3d1706f2 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -147,7 +147,7 @@ Specifies the protocol the `listener` will use * Type: string * Required: required -Allowed values `TCP`, `HTTP`, `HTTPS` +Allowed values are `TCP`, `HTTP`, or `HTTPS` ### listeners.tls * Type: Object From 8a5b597afefc90c60737896ec8fe5177a5e07db0 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:23:13 -0500 Subject: [PATCH 013/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 6d3d1706f2..e526ec000e 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -92,8 +92,8 @@ The `selector` configuration matches zero or more namespaces that determine whic * Required: Required when from is configured to selector. The selector configuration contains one of the following objects: -* matchExpressions -* matchLabels +* `matchExpressions` +* `matchLabels` ### listeners.allowedRoutes.namespaces.selector.matchExpressions Specifies an array of requirements for matching namespaces. If a match is found, then routes from the matching namespace(s) are allowed to attach to the `Gateway`. The following table describes members of the `matchExpressions` array: From 8fbb040e826e85b6e46099edb60b44f32e9199b8 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:23:22 -0500 Subject: [PATCH 014/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index e526ec000e..9ef573435d 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -89,7 +89,7 @@ You can specify one of the following strings: ### listeners.allowedRoutes.namespaces.selector The `selector` configuration matches zero or more namespaces that determine which routes are allowed to attach to the listener. * Type: Object -* Required: Required when from is configured to selector. +* Required: Required when `from` is configured to `Selector`. The selector configuration contains one of the following objects: * `matchExpressions` From a384d0d6676d5337923679bf59bf576142b9975d Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:23:46 -0500 Subject: [PATCH 015/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 9ef573435d..be27c7178c 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -87,7 +87,7 @@ You can specify one of the following strings: * `Selector`: Only routes in namespaces that match the `selector` may be attached. ### listeners.allowedRoutes.namespaces.selector -The `selector` configuration matches zero or more namespaces that determine which routes are allowed to attach to the listener. +Specifies a method of matching namespaces from which routes are allowed to attach to the listener. * Type: Object * Required: Required when `from` is configured to `Selector`. From 50cc6067e9dd27c2db597c1ef3183686bc1f4294 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Fri, 8 Jul 2022 09:54:47 -0500 Subject: [PATCH 016/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index be27c7178c..c99763bae2 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -128,7 +128,7 @@ namespaceSelector: For more on labels, see [Labels and Selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) ### listeners.hostname -Specifies the `listeners` hostname +Specifies the `listener`'s hostname * Type: string * Required: required From f076c8086dad6f44193d3c0af0d2998dd52fe7c6 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 13 Jul 2022 16:01:45 -0500 Subject: [PATCH 017/339] Update website/content/docs/api-gateway/configuration/gatewayclass.mdx Co-authored-by: Nathan Coleman --- .../docs/api-gateway/configuration/gatewayclass.mdx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 5f92df12dd..1988c9a324 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -22,14 +22,13 @@ The following example creates a gateway class called `test-gateway-class`: apiVersion: gateway.networking.k8s.io/v1alpha2 kind: GatewayClass metadata: - name: test-gateway-class + name: test-gateway-class spec: - controllerName: 'hashicorp.com/consul-api-gateway-controller' - parametersRef: - group: api-gateway.consul.hashicorp.com - kind: GatewayClassConfig - name: test-gateway-class-config - ``` + controllerName: 'hashicorp.com/consul-api-gateway-controller' + parametersRef: + group: api-gateway.consul.hashicorp.com + kind: GatewayClassConfig + name: test-gateway-class-config From 8cd1ff2d24bc552363fe9ddb4c04cc4dee155ddb Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 13 Jul 2022 16:01:53 -0500 Subject: [PATCH 018/339] Update website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx Co-authored-by: Nathan Coleman --- .../configuration/gatewayclassconfig.mdx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 3d660d1a7c..ed416430df 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -18,16 +18,15 @@ The following example creates a gateway class configuration called `test-gateway apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 kind: GatewayClassConfig metadata: - name: test-gateway-class-config + name: test-gateway-class-config spec: - useHostPorts: true - logLevel: 'trace' - consul: - scheme: 'https' - ports: - http: 8501 - grpc: 8502 - ``` + useHostPorts: true + logLevel: 'trace' + consul: + scheme: 'https' + ports: + http: 8501 + grpc: 8502 From 45467e141b8c55db8db9180725d668f617ba0a20 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 13 Jul 2022 16:18:39 -0500 Subject: [PATCH 019/339] Update website/content/docs/api-gateway/usage/basic-usage.mdx Co-authored-by: Nathan Coleman --- website/content/docs/api-gateway/usage/basic-usage.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index 9333df2130..92e7829c73 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -25,7 +25,7 @@ description: >- 1. Issue the `kubectl apply` command to implement the configurations, e.g.: ```shell-session -$ kubectl apply -f gateway.yaml routes.yaml +$ kubectl apply --filename gateway.yaml routes.yaml ``` **Note**: For guidance on how to install the Consul K8s CLI, visit the [Installing the Consul K8s CLI](/docs/k8s/installation/install-cli) documentation. +-> **Note**: For guidance on how to install `consul-k8s`, visit the +[Installing the Consul K8s CLI](/docs/k8s/installation/install-cli) documentation. -This topic describes the subcommands and available options for using Consul K8s CLI. +This topic describes the subcommands and available options for using `consul-k8s`. ## Usage @@ -27,11 +27,12 @@ $ consul-k8s You can use the following subcommands with `consul-k8s`. - - [install](#install) - - [uninstall](#uninstall) - - [status](#status) - - [upgrade](#upgrade) BETA - - [version](#version) + - [`install`](#install) + - [`proxy`](#proxy) + - [`status`](#status) + - [`uninstall`](#uninstall) + - [`upgrade`](#upgrade) + - [`version`](#version) ### `install` @@ -68,7 +69,7 @@ The following example command installs Consul according in the `myNS` namespace $ consul-k8s install -preset=secure -namespace=myNS ``` -The following example commands install Consul on Kubernetes using custom values, files, or strings that are set via flags. The underlying Consul-on-Kubernetes Helm chart uses the flags to customize the installation. The flags are comparable to the `helm install` [flags](https://helm.sh/docs/helm/helm_install/#helm-install). +The following example commands install Consul on Kubernetes using custom values, files, or strings that are set via flags. The underlying Consul-on-Kubernetes Helm chart uses the flags to customize the installation. The flags are comparable to the `helm install` [flags](https://helm.sh/docs/helm/helm_install/#helm-install). ```shell-session $ consul-k8s install -set key=value @@ -120,7 +121,7 @@ $ consul-k8s uninstall -namespace=my-ns -name=my-consul -wipe-data=true -auto-ap ### `status` -The `status` command provides an overall status summary of the Consul on Kubernetes installation. It also provides the config that was used to deploy Consul K8s and provides a quick glance at the health of both Consul servers and clients. This command does not take in any flags. +The `status` command provides an overall status summary of the Consul on Kubernetes installation. It also provides the config that was used to deploy Consul K8s and provides a quick glance at the health of both Consul servers and clients. This command does not take in any flags. ```shell-session $ consul-k8s status @@ -165,7 +166,7 @@ $ consul-k8s status ### `upgrade` --> The `consul-k8s upgrade` **subcommand is currently in beta**: This subcommand is not recommended for production environments. +-> The `consul-k8s upgrade` **subcommand is currently in beta**: This subcommand is not recommended for production environments. The `upgrade` command upgrades the Consul on Kubernetes components to the current version of the `consul-k8s` cli. Prior to running `consul-k8s upgrade`, the `consul-k8s` CLI should first be upgraded to the latest version as described [Upgrade the Consul K8s CLI](#upgrade-the-consul-k8s-cli) From 2ec29f6696f563c94f2c1c73ac54ae25a103e383 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Wed, 20 Jul 2022 15:14:14 -0400 Subject: [PATCH 030/339] Move uninstall down to be alphabetically sorted --- website/content/docs/k8s/k8s-cli.mdx | 58 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 5e65292425..51bdf464b3 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -90,35 +90,6 @@ The following example commands install Consul on Kubernetes using custom values, $ consul-k8s install -set-string key=value-bool ``` -### `uninstall` - -The `uninstall` command removes Consul from Kubernetes. - -```shell-session -$ consul-k8s uninstall -``` - -The following options are available. - -| Flag | Description | Default | Required | -| --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------- | -| `-auto-approve`                                     | Boolean value that enables you to skip the removal confirmation prompt. | `false` | Optional | -| `-name` | String value for the name of the installation to remove. | none | Optional | -| `-namespace` | String value that specifies the namespace of the Consul installation to remove. | `consul` | Optional | -| `-timeout` | Specifies how long to wait for the removal process to complete before timing out. The value is specified with an integer and string value indicating a unit of time.
The following units are supported:
`ms` (milliseconds)
`s` (seconds)
`m` (minutes)
`h` (hours)
In the following example, removal will timeout after one minute:
`consul-k8s uninstall -timeout 1m` | `10m` | Optional | -| `-wipe-data` | Boolean value that deletes PVCs and secrets associated with the Consul installation during installation.
Data will be removed without a verification prompt if the `-auto-approve` flag is set to `true`. | `false`
Instructions for removing data will be printed to the console. | Optional | -| `--help` | Prints usage information for this option. | none | Optional | - -See [Global Options](#global-options) for additional commands that you can use when uninstalling Consul from Kubernetes. - -#### Example Command - -The following example command immediately uninstalls Consul from the `my-ns` namespace with the name `my-consul` and removes PVCs and secrets associated with the installation without asking for verification: - -```shell-session -$ consul-k8s uninstall -namespace=my-ns -name=my-consul -wipe-data=true -auto-approve=true -``` - ### `status` The `status` command provides an overall status summary of the Consul on Kubernetes installation. It also provides the config that was used to deploy Consul K8s and provides a quick glance at the health of both Consul servers and clients. This command does not take in any flags. @@ -164,6 +135,35 @@ $ consul-k8s status ✓ Consul clients healthy (3/3) ``` +### `uninstall` + +The `uninstall` command removes Consul from Kubernetes. + +```shell-session +$ consul-k8s uninstall +``` + +The following options are available. + +| Flag | Description | Default | Required | +| --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------- | +| `-auto-approve`                                     | Boolean value that enables you to skip the removal confirmation prompt. | `false` | Optional | +| `-name` | String value for the name of the installation to remove. | none | Optional | +| `-namespace` | String value that specifies the namespace of the Consul installation to remove. | `consul` | Optional | +| `-timeout` | Specifies how long to wait for the removal process to complete before timing out. The value is specified with an integer and string value indicating a unit of time.
The following units are supported:
`ms` (milliseconds)
`s` (seconds)
`m` (minutes)
`h` (hours)
In the following example, removal will timeout after one minute:
`consul-k8s uninstall -timeout 1m` | `10m` | Optional | +| `-wipe-data` | Boolean value that deletes PVCs and secrets associated with the Consul installation during installation.
Data will be removed without a verification prompt if the `-auto-approve` flag is set to `true`. | `false`
Instructions for removing data will be printed to the console. | Optional | +| `--help` | Prints usage information for this option. | none | Optional | + +See [Global Options](#global-options) for additional commands that you can use when uninstalling Consul from Kubernetes. + +#### Example Command + +The following example command immediately uninstalls Consul from the `my-ns` namespace with the name `my-consul` and removes PVCs and secrets associated with the installation without asking for verification: + +```shell-session +$ consul-k8s uninstall -namespace=my-ns -name=my-consul -wipe-data=true -auto-approve=true +``` + ### `upgrade` -> The `consul-k8s upgrade` **subcommand is currently in beta**: This subcommand is not recommended for production environments. From 5215b63598579151b9c620e443faeec5535af8ec Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:00:04 -0500 Subject: [PATCH 031/339] Update website/content/docs/api-gateway/usage/basic-usage.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/usage/basic-usage.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index 864356e578..ff57d8b7c7 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -124,7 +124,7 @@ Error: INSTALLATION FAILED: unable to build kubernetes objects from release mani When this error occurs during the process of installing Consul API Gateway, it is usually caused by not having the required CRD files installed in Kubernetes prior to installing Consul API Gateway. **Impact:** -The installation process will typically fail after this error message is generated +The installation process typically fails after this error message is generated. **Recommended Action:** Install the required CRDs by using the command in Step 1 of the [Consul API Gateway installation instructions](/docs/api-gateway/consul-api-gateway-install) and then retry installing Consul API Gateway. From c5e4923640eff65786615afe89932f831e0e0252 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:02:09 -0500 Subject: [PATCH 032/339] Update website/content/docs/api-gateway/usage/basic-usage.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/usage/basic-usage.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index ff57d8b7c7..9243e66520 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -77,9 +77,7 @@ consul-api-gateway 0.1.0 ``` ---> -## Common Error Messages - -Some of the errors messages commonly encountered during installation and operations of Consul API Gateway are listed below, along with suggested methods for resolving them. +## Error Messages If the error message is not listed on this page, it may be listed on the main [Consul Common errors][consul-common-errors] page. If the error message is not listed on that page either, please consider following our general [Troubleshooting Guide][troubleshooting] or reach out to us in [Discuss](https://discuss.hashicorp.com/). From 381a987549a6e27a6cce0cffc2254f13ea0124bd Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:02:20 -0500 Subject: [PATCH 033/339] Update website/content/docs/api-gateway/usage/basic-usage.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/usage/basic-usage.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index 9243e66520..7576cd26aa 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -10,7 +10,7 @@ description: >- 1. Verify that the [requirements](/docs/api-gateway/consul-api-gateway-install#requirments) have been met. 1. Verify that the Consul API Gateway CRDs and controller have been installed and applied (see [Installation](/docs/api-gateway/consul-api-gateway-install)). -1. Configure the artifacts described below in [Configuration](/docs/api-gateway/configuration). +1. Configure the artifacts as describe in the [Configuration](/docs/api-gateway/configuration) section. From f1a6067a0be7e383d3be93e1c7c5f28802e10186 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:03:56 -0500 Subject: [PATCH 034/339] Update website/data/docs-nav-data.json Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/data/docs-nav-data.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 1ce60ffc9a..786a095315 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -431,11 +431,11 @@ "path": "api-gateway/configuration/gateway" }, { - "title": "Gateway Class", + "title": "GatewayClass", "path": "api-gateway/configuration/gatewayclass" }, { - "title": "Gateway Class Config", + "title": "GatewayClassConfig", "path": "api-gateway/configuration/gatewayclassconfig" }, { From c859af7ad951ba5024d0ea94492a4fcbb236283a Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Wed, 20 Jul 2022 16:04:53 -0500 Subject: [PATCH 035/339] Update website/content/docs/api-gateway/install.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/install.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/install.mdx b/website/content/docs/api-gateway/install.mdx index 298366ef4c..54dd1d6e1a 100644 --- a/website/content/docs/api-gateway/install.mdx +++ b/website/content/docs/api-gateway/install.mdx @@ -23,7 +23,8 @@ Ensure that the environment you are deploying Consul API Gateway in meets the re $ kubectl apply --kustomize="github.com/hashicorp/consul-api-gateway/config/crd?ref=vVERSION" ``` -1. Create a `values.yaml` file for your Consul API Gateway deployment. Copy the content below into the `values.yaml` file. The `values.yaml` will be used by the Consul Helm chart. Available versions of the [Consul](https://hub.docker.com/r/hashicorp/consul/tags) and [Consul API Gateway](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags) Docker images can be found on DockerHub, with additional context on version compatibility published in [GitHub releases](https://github.com/hashicorp/consul-api-gateway/releases). See [Helm Chart Configuration - apiGateway](https://www.consul.io/docs/k8s/helm#apigateway) for more available options on how to configure your Consul API Gateway deployment via the Helm chart. +1. Create a `values.yaml` file for your Consul API Gateway deployment. +1. Copy the following example configuration into the `values.yaml` file. The Consul Helm chart uses the `values.yaml` file to install Consul. @@ -37,10 +38,11 @@ Ensure that the environment you are deploying Consul API Gateway in meets the re apiGateway: enabled: true image: hashicorp/consul-api-gateway:VERSION - ``` + ``` - + + Refer to the [Consul](https://hub.docker.com/r/hashicorp/consul/tags) and [Consul API Gateway] (https://hub.docker.com/r/hashicorp/consul-api-gateway/tags) DockerHub pages for information about all available versions, as well as additional context about version compatibility published in [GitHub releases](https://github.com/hashicorp/consul-api-gateway/releases). See [Helm Chart Configuration - apiGateway](https://www.consul.io/docs/k8s/helm#apigateway) for more available options on how to configure your Consul API Gateway deployment via the Helm chart. 1. Install Consul API Gateway using the standard Consul Helm chart and specify the custom values file. Available versions of the [Consul Helm chart](https://github.com/hashicorp/consul-k8s/releases) can be found in GitHub releases. ```shell-session From 7cbc1d91382505f143223296c3fe94aa5b973768 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Wed, 20 Jul 2022 16:21:14 -0500 Subject: [PATCH 036/339] fix indent issue --- .../docs/api-gateway/configuration/routes.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index 32d314cec1..13e720f56e 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -23,15 +23,15 @@ The following example creates a route named `example-route` associated with a li apiVersion: gateway.networking.k8s.io/v1alpha2 kind: HTTPRoute metadata: - name: example-route + name: example-route spec: - parentRefs: - - name: example-gateway - rules: - - backendRefs: - - kind: Service - name: echo - port: 8080 + parentRefs: + - name: example-gateway + rules: + - backendRefs: + - kind: Service + name: echo + port: 8080 ``` From 9feb465f62f8f4767e9488e225ae91e44ed06e0c Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:39:25 -0500 Subject: [PATCH 037/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 635843d4ba..f88ac19335 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -21,7 +21,7 @@ Specify the following parameters to declare a Gateway: |:-----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| | `kind` | Specifies the type of configuration object. The value should always be Gateway | Required | | `description` | Human-readable string for describing the purpose of the Gateway. | Optional | -| `version ` | Specifies the Kubernetes API version. The value should always be gateway.networking.k8s.io/v1alpha2 | Required | +| `version ` | Specifies the Kubernetes API version. The value should always be `gateway.networking.k8s.io/v1alpha2` | Required | | `scope` | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | | `fields` | Specifies the configurations for the Gateway. The fields are listed in the Configuration model. Details for each field are described in the Specification. | Required | From dfc9ae4a609b3cfbfa141c65865fc604d29dcc93 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:39:34 -0500 Subject: [PATCH 038/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index f88ac19335..affe2cb5fc 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -20,7 +20,7 @@ Specify the following parameters to declare a Gateway: | Parameter | Description | Required | |:-----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| | `kind` | Specifies the type of configuration object. The value should always be Gateway | Required | -| `description` | Human-readable string for describing the purpose of the Gateway. | Optional | +| `description` | Human-readable string that describes the purpose of the `Gateway`. | Optional | | `version ` | Specifies the Kubernetes API version. The value should always be `gateway.networking.k8s.io/v1alpha2` | Required | | `scope` | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | | `fields` | Specifies the configurations for the Gateway. The fields are listed in the Configuration model. Details for each field are described in the Specification. | Required | From 5d02480430026905565c89ee96f4789df5dc3f7e Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:39:47 -0500 Subject: [PATCH 039/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index affe2cb5fc..be1de030b7 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -19,7 +19,7 @@ Specify the following parameters to declare a Gateway: | Parameter | Description | Required | |:-----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| -| `kind` | Specifies the type of configuration object. The value should always be Gateway | Required | +| `kind` | Specifies the type of configuration object. The value should always be `Gateway`. | Required | | `description` | Human-readable string that describes the purpose of the `Gateway`. | Optional | | `version ` | Specifies the Kubernetes API version. The value should always be `gateway.networking.k8s.io/v1alpha2` | Required | | `scope` | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | From c54e0904ded357fd27dd7d1f2a64472e70c6220e Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Thu, 21 Jul 2022 14:39:55 -0500 Subject: [PATCH 040/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index be1de030b7..2829ef03eb 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -18,7 +18,7 @@ Gateway instances derive their configurations from the [`GatewayClass`](/docs/ap Specify the following parameters to declare a Gateway: | Parameter | Description | Required | -|:-----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| +| -----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| | `kind` | Specifies the type of configuration object. The value should always be `Gateway`. | Required | | `description` | Human-readable string that describes the purpose of the `Gateway`. | Optional | | `version ` | Specifies the Kubernetes API version. The value should always be `gateway.networking.k8s.io/v1alpha2` | Required | From 20e97a7729bf8de9c7f3be5fee44519a361775d7 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Thu, 21 Jul 2022 14:53:55 -0500 Subject: [PATCH 041/339] merge back in mike's environment doc in install --- website/content/docs/api-gateway/install.mdx | 52 +++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/website/content/docs/api-gateway/install.mdx b/website/content/docs/api-gateway/install.mdx index 54dd1d6e1a..11ee46f1f6 100644 --- a/website/content/docs/api-gateway/install.mdx +++ b/website/content/docs/api-gateway/install.mdx @@ -15,39 +15,43 @@ Ensure that the environment you are deploying Consul API Gateway in meets the re ## Installation --> **Version reference convention:** Replace `VERSION` in command and configuration examples with the Consul API Gateway version you are installing, such as `0.3.0`. In some instances, `VERSION` is prepended with a lowercase _v_. This indicates that you must include the `v` as is part of the version, for example `v0.3.0`. +1. Set the version of Consul API Gateway you are installing as an environment variable. The following steps use this environment variable in commands and configurations. + +```shell-session +$ export VERSION=0.3.0 +``` 1. Issue the following command to install the CRDs: - ```shell-session - $ kubectl apply --kustomize="github.com/hashicorp/consul-api-gateway/config/crd?ref=vVERSION" - ``` +```shell-session +$ kubectl apply --kustomize="github.com/hashicorp/consul-api-gateway/config/crd?ref=v$VERSION" +``` -1. Create a `values.yaml` file for your Consul API Gateway deployment. -1. Copy the following example configuration into the `values.yaml` file. The Consul Helm chart uses the `values.yaml` file to install Consul. +1. Create a `values.yaml` file for your Consul API Gateway deployment by copying the following content and running it in the environment where you set the `VERSION` environment variable. The Consul Helm chart uses this `values.yaml` file to deploy the API Gateway. Available versions of the [Consul](https://hub.docker.com/r/hashicorp/consul/tags) and [Consul API Gateway](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags) Docker images can be found on DockerHub, with additional context on version compatibility published in [GitHub releases](https://github.com/hashicorp/consul-api-gateway/releases). For more options to configure your Consul API Gateway deployment through the Helm chart, refer to [Helm Chart Configuration - apiGateway](https://www.consul.io/docs/k8s/helm#apigateway). - + - ```yaml - global: - name: consul - connectInject: - enabled: true - controller: - enabled: true - apiGateway: - enabled: true - image: hashicorp/consul-api-gateway:VERSION - ``` + ```shell + cat < values.yaml + global: + name: consul + connectInject: + enabled: true + controller: + enabled: true + apiGateway: + enabled: true + image: hashicorp/consul-api-gateway:$VERSION + EOF + ``` + + - - - Refer to the [Consul](https://hub.docker.com/r/hashicorp/consul/tags) and [Consul API Gateway] (https://hub.docker.com/r/hashicorp/consul-api-gateway/tags) DockerHub pages for information about all available versions, as well as additional context about version compatibility published in [GitHub releases](https://github.com/hashicorp/consul-api-gateway/releases). See [Helm Chart Configuration - apiGateway](https://www.consul.io/docs/k8s/helm#apigateway) for more available options on how to configure your Consul API Gateway deployment via the Helm chart. 1. Install Consul API Gateway using the standard Consul Helm chart and specify the custom values file. Available versions of the [Consul Helm chart](https://github.com/hashicorp/consul-k8s/releases) can be found in GitHub releases. - ```shell-session - $ helm install consul hashicorp/consul --version 0.45.0 --values values.yaml --create-namespace --namespace consul - ``` +```shell-session +$ helm install consul hashicorp/consul --version 0.45.0 --values values.yaml --create-namespace --namespace consul + ``` +[tech-specs]: /docs/api-gateway/tech-specs [rel-notes]: /docs/release-notes From 355f6dbd48680bf40c19e435c0fcb24f5a5f38ad Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Fri, 22 Jul 2022 09:45:00 -0500 Subject: [PATCH 049/339] Update website/content/docs/api-gateway/usage/basic-usage.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/usage/basic-usage.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index 7576cd26aa..d5cd194776 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -119,7 +119,7 @@ REPLACE THIS with the actions the user should take to try to correct the situati Error: INSTALLATION FAILED: unable to build kubernetes objects from release manifest: [unable to recognize "": no matches for kind "GatewayClass" in version "gateway.networking.k8s.io/v1alpha2", unable to recognize "": no matches for kind "GatewayClassConfig" in version "api-gateway.consul.hashicorp.com/v1alpha1"] ``` **Conditions:** -When this error occurs during the process of installing Consul API Gateway, it is usually caused by not having the required CRD files installed in Kubernetes prior to installing Consul API Gateway. +Consul API Gateway generates this error when the required CRD files have not been installed in Kubernetes prior to installing Consul API Gateway. **Impact:** The installation process typically fails after this error message is generated. From 16c85630496b6e63cd32b4d4948ba7e315af84db Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Fri, 22 Jul 2022 12:14:01 -0400 Subject: [PATCH 050/339] Add descriptions to the subjects --- website/content/docs/k8s/k8s-cli.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index ebd50de09d..0feeab6e6c 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -23,18 +23,18 @@ Consul K8s CLI uses the following syntax: $ consul-k8s ``` -## Subcommands +## Commands -You can use the following subcommands with `consul-k8s`. +You can use the following commands with `consul-k8s`. - [`install`](#install) installs Consul to your Kubernetes cluster. - [`proxy`](#proxy) allows you to interact with proxies managed by Consul on your Kubernetes cluster. - - [`proxy list`](#proxy-list) - - [`proxy read`](#proxy-read) - - [`status`](#status) displays the - - [`uninstall`](#uninstall) - - [`upgrade`](#upgrade) - - [`version`](#version) + - [`proxy list`](#proxy-list) displays all relevant proxies. + - [`proxy read`](#proxy-read) displays the configuration of proxies on a given Pod. + - [`status`](#status) displays the status of your Consul installation along with its configuration. + - [`uninstall`](#uninstall) uninstalls Consul from your Kubernetes cluster. + - [`upgrade`](#upgrade) modifies your Consul installation's configuration. + - [`version`](#version) displays the version of Consul on Kubernetes that is installed. ### `install` From acf4758e1207b33ec0f5a0254c50d2e546746e8a Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Fri, 22 Jul 2022 13:43:38 -0400 Subject: [PATCH 051/339] Add options and examples to proxy read --- website/content/docs/k8s/k8s-cli.mdx | 172 ++++++++++++++++++++++++++- 1 file changed, 167 insertions(+), 5 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 0feeab6e6c..66351d7e1a 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -130,8 +130,7 @@ This command will list proxies alongside their `Type`. Types of proxies include #### Example Commands Display all pods in the current Kubernetes namespace which run proxies managed -by Consul. Note that `Sidecar` pods are pods which are running the proxy in a -sidecar pattern and are services running in the mesh. +by Consul. ```shell-session $ consul-k8s proxy list @@ -149,9 +148,7 @@ frontend-676564547c-v2mfq Sidecar ``` Display all pods in the `consul` Kubernetes namespace which run proxies managed -by Consul. Note that these pods are labeled with the type `Ingress Gateway`. -They run a proxy managed by Consul for controlling ingress into the Kubernetes -cluster. +by Consul. ```shell-session $ consul-k8s proxy list -n consul @@ -186,6 +183,171 @@ default frontend-676564547c-v2mfq Sidecar ### `proxy read` +The `proxy read` command allows you to inspect the configuration of any Envoy proxies running on a given Pod. + +```shell-session +$ consul-k8s proxy read +``` + +The command takes a required value, ``. This should be the full name +of a Kubernetes Pod. + +The following options are available. + +| Flag | Description | Default | Required | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- | +| `-namespace`, `-n`         | `String` The namespace where the target Pod can be found. | Current [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) namespace. | Optional | +| `-output`, `-o` | `String` Output the Envoy configuration as 'table', 'json', or 'raw'. | `'table'` | Optional | +| `-clusters` | `Boolean` Filter output to only show clusters. | `false` | Optional | +| `-endpoints` | `Boolean` Filter output to only show endpoints. | `false` | Optional | +| `-listeners` | `Boolean` Filter output to only show listeners. | `false` | Optional | +| `-routes` | `Boolean` Filter output to only show routes. | `false` | Optional | +| `-secrets` | `Boolean` Filter output to only show secrets. | `false` | Optional | +| `-address` | `String` Filter clusters, endpoints, and listeners output to only those with endpoint addresses which contain the given value. | `""` | Optional | +| `-fqdn` | `String` Filter cluster output to only clusters with a fully qualified domain name which contains the given value. | `""` | Optional | +| `-port` | `Int` Filter endpoints output to only endpoints with the given port number. | `-1` which does not filter by port | Optional | + +#### Example commands + +Get the configuration summary for the Envoy proxy running on the Pod +`backend-658b679b45-d5xlb`. + +```shell-session +$ consul-k8s proxy read backend-658b679b45-d5xlb +``` + +``` +Envoy configuration for backend-658b679b45-d5xlb in namespace default: + +==> Clusters (5) +Name FQDN Endpoints Type Last Updated +local_agent local_agent 192.168.79.187:8502 STATIC 2022-05-13T04:22:39.553Z +client client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul EDS 2022-07-21T12:12:27.335Z +frontend frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul EDS 2022-07-21T12:12:27.242Z +local_app local_app 127.0.0.1:8080 STATIC 2022-05-13T04:22:39.655Z +original-destination original-destination ORIGINAL_DST 2022-05-13T04:22:39.743Z + + +==> Endpoints (6) +Address:Port Cluster Weight Status +192.168.79.187:8502 local_agent 1.00 HEALTHY +192.168.18.110:20000 1.00 HEALTHY +192.168.52.101:20000 1.00 HEALTHY +192.168.65.131:20000 1.00 HEALTHY +192.168.63.120:20000 1.00 HEALTHY +127.0.0.1:8080 local_app 1.00 HEALTHY + + +==> Listeners (2) +Name Address:Port Direction Filter Chain Match Filters Last Updated +public_listener 192.168.69.179:20000 INBOUND Any * -> local_app/ 2022-07-21T12:12:42.148Z +outbound_listener 127.0.0.1:15001 OUTBOUND 10.100.134.173/32, 240.0.0.3/32 -> client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul 2022-07-18T15:31:03.246Z + 10.100.31.2/32, 240.0.0.5/32 -> frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul + Any -> original-destination + + +==> Routes (1) +Name Destination Cluster Last Updated +public_listener local_app/ 2022-07-21T12:12:42.147Z + + +==> Secrets (0) +Name Type Last Updated + +``` + +Get the Envoy configuration summary for all clusters with a fully qualified +domain name which includes `"default"`. Display only clusters and listeners. + +```shell-session +$ consul-k8s proxy read backend-658b679b45-d5xlb -fqdn default -clusters -listeners +``` + +``` +Envoy configuration for backend-658b679b45-d5xlb in namespace default: + +==> Filters applied + Fully qualified domain names containing: default + +==> Clusters (2) +Name FQDN Endpoints Type Last Updated +client client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul EDS 2022-07-21T12:12:27.335Z +frontend frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul EDS 2022-07-21T12:12:27.242Z + + +==> Listeners (2) +Name Address:Port Direction Filter Chain Match Filters Last Updated +public_listener 192.168.69.179:20000 INBOUND Any * -> local_app/ 2022-07-21T12:12:42.148Z +outbound_listener 127.0.0.1:15001 OUTBOUND 10.100.134.173/32, 240.0.0.3/32 -> client.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul 2022-07-18T15:31:03.246Z + 10.100.31.2/32, 240.0.0.5/32 -> frontend.default.dc1.internal.bc3815c2-1a0f-f3ff-a2e9-20d791f08d00.consul + Any -> original-destination + +``` + +Get the raw Envoy configuration dump for the Envoy proxy running on the Pod +`backend-658b679b45-d5xlb`. The raw configuration will be output for each +service as a JSON map. The [JQ command line tool](https://stedolan.github.io/jq/) +can be used to index into the configuration for the service you want to inspect. + +See the [Envoy config dump documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/config_dump.proto) +for more information on the structure of the config dump. + +```shell-session +$ consul-k8s proxy read backend-658b679b45-d5xlb -o raw +``` + +``` +{ + "backend-658b679b45-d5xlb": { + "configs": [ + { + "@type": "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump", + "bootstrap": { + // [-- snip 1201 lines --] + }, + "last_updated": "2022-05-13T04:22:39.488Z" + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.ClustersConfigDump", + "static_clusters": [ + // [-- snip 42 lines --] + ], + "dynamic_active_clusters": [ + // [-- snip 144 lines --] + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump", + "static_endpoint_configs": [ + // [-- snip 29 lines --] + ], + "dynamic_endpoint_configs": [ + // [-- snip 120 lines --] + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.ListenersConfigDump", + "dynamic_listeners": [ + // [-- snip 216 lines --] + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.ScopedRoutesConfigDump" + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.RoutesConfigDump", + "static_route_configs": [ + // [-- snip 25 lines --] + ] + }, + { + "@type": "type.googleapis.com/envoy.admin.v3.SecretsConfigDump" + } + ] + } +} +``` + ### `status` From 082ad42ff43fe0dce13d0ab9dd85c47937afadaf Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Fri, 22 Jul 2022 14:20:27 -0500 Subject: [PATCH 052/339] add redirects --- website/redirects.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/redirects.js b/website/redirects.js index c2f751df78..c25cf8ccc3 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -1319,4 +1319,14 @@ module.exports = [ destination: '/docs/k8s/installation/vault/data-integration/connect-ca', permanent: true, }, + { + source: '/docs/api-gateway/common-errors', + destination: '/docs/api-gateway/usage/basic-usage#Common_Error_Messages', + permanent: true, + }, + { + source: '/docs/api-gateway/upgrade-specific-versions', + destination: '/docs/api-gateway/upgrades', + permanent: true, + }, ] From af56187fb770809af4a0023b02a1660dcbf21f52 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Mon, 25 Jul 2022 20:30:51 -0500 Subject: [PATCH 053/339] left align table --- website/content/docs/api-gateway/configuration/gateway.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 2829ef03eb..fda00c9066 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -17,8 +17,8 @@ Gateway instances derive their configurations from the [`GatewayClass`](/docs/ap Specify the following parameters to declare a Gateway: -| Parameter | Description | Required | -| -----------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------:| +| Parameter | Description | Required | +| :----------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------- |:-------- | | `kind` | Specifies the type of configuration object. The value should always be `Gateway`. | Required | | `description` | Human-readable string that describes the purpose of the `Gateway`. | Optional | | `version ` | Specifies the Kubernetes API version. The value should always be `gateway.networking.k8s.io/v1alpha2` | Required | From 8fe5c14d80b2e7326e79cad25d96ab6387c354f6 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Mon, 25 Jul 2022 21:57:30 -0500 Subject: [PATCH 054/339] updated all pages to follow cm/s specification --- .../api-gateway/configuration/gateway.mdx | 44 ++--- .../configuration/gatewayclass.mdx | 51 ++++- .../configuration/gatewayclassconfig.mdx | 177 +++++++++++++++--- 3 files changed, 227 insertions(+), 45 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index fda00c9066..b340d3c329 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -29,27 +29,27 @@ Specify the following parameters to declare a Gateway: ## Configuration model -* `gatewayClassName`: string | required -* `listeners`: array of objects | required - * `allowedRoutes`: object | required - * `namespaces`: object | required - * `from`: string | required - * `selector`: object | required if from is configured to selector - * `matchExpressions`: array of objects | required if matchLabels is not configured - * `key`: string | required if matchExpressions is declared - * `operator`: string | required if matchExpressions is declared - * `values`: array of strings | required if matchExpressions is declared - * `matchLabels`: map of strings | required if matchExpressions is not configured - * `hostname`: string | required - * `name`: string | required - * `port`: integer | required - * `protocol`: string | required - * `tls`: object | required if protocol is set to HTTPS - * `certificateRefs`: array or objects | required if tls is declared - * `name`: string | required if certificateRefs is declared - * `namespace`: string | required if certificateRefs is declared - * `mode`: string | required if certificateRefs is declared - * `options`: map of strings | optional +* (`gatewayClassName`)[###gatewayClassName]: string | required +* (`listeners`)[###listeners]: array of objects | required + * (`allowedRoutes`)[###listeners.allowedRoutes]: object | required + * (`namespaces`}[###listeners.namespaces]: object | required + * (`from`)[###listeners.namespaces.from]: string | required + * (`selector`)[###listeners.namespaces.selector]: object | required if from is configured to selector + * (`matchExpressions`)[###listeners.namespaces.selector.matchExpressions]: array of objects | required if matchLabels is not configured + * (`key`)[###listeners.namespaces.selector.matchExpressions.key]: string | required if matchExpressions is declared + * (`operator`)[###listeners.namespaces.selector.operator]: string | required if matchExpressions is declared + * (`values`)[###listeners.namespaces.selector.values]: array of strings | required if matchExpressions is declared + * (`matchLabels`)[###listeners.namespaces.selector.matchLabels]: map of strings | required if matchExpressions is not configured + * (`hostname`)[###listeners.hostname]: string | required + * (`name`)[###listeners.name]: string | required + * (`port`)[###listeners.port]: integer | required + * (`protocol)[###listeners.protocol]`: string | required + * (`tls`)[###listeners.tls]: object | required if protocol is set to HTTPS + * (`certificateRefs`)[###listeners.tls.certificateRefs]: array or objects | required if tls is declared + * (`name`)[###listeners.tls.certificateRefs.name]: string | required if certificateRefs is declared + * (`namespace`)[###listeners.tls.certificateRefs.namespace]: string | required if certificateRefs is declared + * (`mode`)[###listeners.tls.mode]: string | required if certificateRefs is declared + * (`options`)[###listeners.tls.options]: map of strings | optional ## Specification @@ -99,7 +99,7 @@ The selector configuration contains one of the following objects: Specifies an array of requirements for matching namespaces. If a match is found, then routes from the matching namespace(s) are allowed to attach to the `Gateway`. The following table describes members of the `matchExpressions` array: | Requirement | Description | Type | -|:-----------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------:| +|:----------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |:---------------- | |`key` | Specifies that label that the key applies to. | string | |`operator` | Specifies the key's relation to a set of values. The following values are valid:In: description of what this means NotIn: description of what this means Exists: description of what this means DoesNotExist: description of what this means | string | |`values` | Specifies an array of string values. If the operator is configured to In or NotIn,the values array must be non-empty. If theoperator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. | array of strings | diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 33fa17b193..2a5104f518 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -7,13 +7,62 @@ description: >- # GatewayClass +This topic provides full details about the `GatewayClass` resource + +## Introduction + The `GatewayClass` resource is used as a template for creating `Gateway` resources. The specification includes the name of the controller (`controllerName`) and an API object containing controller-specific configuration resources within the cluster (`parametersRef`). The value of the `controllerName` field must be set to `hashicorp.com/consul-api-gateway-controller`. When gateways are created from a `GatewayClass`, they use the parameters specified in the `GatewayClass` at the time of instantiation. -Add the `kind: GatewayClass` option to the the gateway values file to declare a gateway class. +The `GatewayClass` resource is a generic kubernetes gateway object. For configuration specific to Consul API Gateway, see [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig). + +## Configuration model + +* (`controllerName`)[###controllerName]: string | required +* (`parametersRef`)[###parametersRef]: object | optional + * (`group`)[###parametersRef.group]: Group | required is parametersRef is set + * (`kind`)[###parametersRef.kind]: Kind | required is parametersRef is set + * (`name`)[###parametersRef.name]: string | required is parametersRef is set +* (`description`)[###description]: string | optional + +## Specification + +This topic provides details about the configuration parameters. + +### controllerName +The name of the controller that is managing the gateways of this class. When using Consul API Gateway, this value should be equal to `'hashicorp.com/consul-api-gateway-controller'` +* Type: string +* Required: required + +### parametersRef +An object that defines additional configuration required by the gateway controller. +* Type: object +* Required: required + +### parametersRef.group +When using Consul API Gateway, this value should be equal to `api-gateway.consul.hashicorp.com` +* Type: Group +* Required: required + +### parametersRef.kind +When using Consul API Gateway, this value should be equal to `GatewayClassConfig` +* Type: object +* Required: required + +### parametersRef.name +The name of the `GatewayClass` +* Type: object +* Required: required + +### description +Helps describe a gateway class with more details +* Type: string +* Required: optional + +## Complete Configuration The following example creates a gateway class called `test-gateway-class`: diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 3cdebb1884..5b35c04aa7 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -7,9 +7,156 @@ description: >- # GatewayClassConfig +This topic provides full details about the `GatewayClassConfig` resource + +## Introduction + The `GatewayClassConfig` object describes Consul API Gateway-related configuration parameters for the [`GatewayClass`](#gatewayclass). -Add the `kind: GatewayClassConfig` option to the gateway values file to declare a gateway class. +A `GatewayClassConfig` object provides configuration options specifc to Consul API Gateway + +## Configuration model + +* (`consul`)[###consul]: object | optional + * (`address`)[###consul.address] : string | optional + * (`authentication`)[###consul.authentication]: object | optional + * (`account`)[[###consul.authentication.account] : string | optional + * (`managed`)[###consul.authentication.managed] : bool | optional + * (`method`)[###consul.authentication.method] : string | optional + * (`namespace`)[###consul.authentication.namespace] : string | optional + * (`ports`)[###consul.ports] : object | optional + * (`grpc`)[###consul.ports.grpc] : integer | optional + * (`http`)[###consul.ports.http] : integer | optional + * (`scheme`)[###consul.scheme] : string | optional +* (`copyAnnotations`)[###copyAnnotations] : object | optional + * (`service`)[###copyAnnotations.service] : array of strings | optional +* (`deployment`)[###deployment] : object | optional + * (`defaultInstances`)[###deployment.defaultInstances] : integer | optional + * (`maxInstances`)[###deployment.maxInstances] : integer | optional + * (`minInstances`)[###deployment.minInstances] : integer | optional +* (`image`)[###image] : object | optional + * (`consulAPIGateway`)[###image.consulAPIGateway] : string | optional + * (`envoy`)[###image.envoy] : string | optional +* (`logLevel`)[###logLevel] : string | optional +* (`nodeSelector`)[###nodeSelector] : string | optional +* (`serviceType`)[###serviceType] : string | optional +* (`useHostPorts`)[###useHostPorts] : boolean | optional + +## Specification + +This topic provides details about the configuration parameters. + +### consul +* Type: object +* Required: optional + +### consul.address +Specifies the address of the Consul server that the `Gateway` communicates with in the gateway pod. If unspecified, the pod attempts to use a local agent on the host where the pod is running. +* Type: string +* Required: optional +* Default: local agent + +### consul.authentication.account +Specifies the Kubernetes service account to use for authentication. +* Type: string +* Required: optional + +### consul.authentication.managed +Set to `true` to enable deployments to run with managed service accounts created by the gateway controller. The `consul.authentication.account` field is ignored when this option is enabled. +* Type: boolean +* Required: optional +* Default: `false` + +### consul.authentication.method +Specifies the Consul auth method used for initial authentication by Consul API Gateway. +* Type: string +* Required: optional + +### consul.authentication.namespace +Specifies the Consul namespace to use for authentication. +* Type: string +* Required: optional + +### consul.ports.grpc +Specifies the gRPC port for Consul's xDS server. +* Type: integer +* Required: optional +* Default: `8502` + +### consul.ports.http +Specifies the Consul namespace to use for authentication. +* Type: integer +* Required: optional +* Default: `8500` + +### consul.scheme +Specifies the scheme to use for connecting to Consul. The supported values are `"http"` and `"https"`. +* Type: string +* Required: optional +* Default: `http` + +### copyAnnotations.service +List of kubernetes (annotations)[https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/] to copy to the gateway service. +* Type: Array of Strings +* Required: optional + +### deployment.defaultInstances +Specifies the number of gateway instances to deploy per gateway configuration +* Type: Integer +* Required: optional +* Default: 1 + +### deployment.maxInstances +Specifies the maximum allowed number of gateway instances per gateway configuration +* Type: Integer +* Required: optional +* Default: 8 + +### deployment.minInstances +Specifies the minimum allowed number of gateway instances per gateway configuration +* Type: Integer +* Required: optional +* Default: 1 + +### image.consulAPIGateway +The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). + +Most deployments will use the default value unless a specific version of the Consul API Gateway is desired. +* Type: string +* Required: optional +* Default: `"hashicorp/consul-api-gateway:RELEASE_VERSION"` + +### image.envoy +The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). + +Most deployments will use the default value unless a specific version of Envoy is desired. +* Type: string +* Required: optional +* Default: `"envoyproxy/envoy:RELEASE_VERSION"` + +### logLevel +Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. +* Type: string +* Required: optional +* Default: `info` + +### nodeSelector +Specifies a set of parameters that constrain the nodes on which the pod can run. Defining nodes with the `nodeSelector` enables the pod to fit on a node. The selector must match a node's labels for the pod to be scheduled on that node. Refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) for additional information.* Type: string +* Required: optional + +### serviceType +Specifies the ingress methods for a service. The following values are supported:
`ClusterIP`
`NodePort`
`LoadBalancer`. +* Type: string +* Required: optional + +### useHostPorts +If set to `true`, then the Envoy container ports are mapped to host ports. +* Type: boolean +* Required: optional +* Default: `false` + + +## Full Configuration The following example creates a gateway class configuration called `test-gateway-class-config`: @@ -30,27 +177,13 @@ The following example creates a gateway class configuration called `test-gateway ``` -The following table describes the allowed parameters for the `spec` array: -| Parameter | Description | Type | Default | -| --- | --- | ---- | ------- | -| `consul.address` | Specifies the address of the Consul server that the `Gateway` communicates with in the gateway pod. If unspecified, the pod attempts to use a local agent on the host where the pod is running. | String | A local agent on the host where the pod is running. | -| `consul.authentication.account` | Specifies the Kubernetes service account to use for authentication. | String | N/A | -| `consul.authentication.managed` | Set to `true` to enable deployments to run with managed service accounts created by the gateway controller. The `consul.authentication.account` field is ignored when this option is enabled. | Boolean | `false` | -| `consul.authentication.method` | Specifies the Consul auth method used for initial authentication by Consul API Gateway. | String | N/A | -| `consul.authentication.namespace` | Specifies the Consul namespace to use for authentication. | String | N/A | -| `consul.ports.grpc` | Specifies the gRPC port for Consul's xDS server. | Integer | `8502` | -| `consul.ports.http` | Specifies the port for Consul's HTTP server. | Integer | `8500` | -| `consul.scheme` | Specifies the scheme to use for connecting to Consul. The supported values are `"http"` and `"https"`. | String | `"http"` | -| `copyAnnotations.service` | List of annotations to copy to the gateway service. | Array | `["external-dns.alpha.kubernetes.io/hostname"]` | -| `deployment.defaultInstances` | Specifies the number of instances to deploy by default for each gateway. | Integer | 1 | -| `deployment.maxInstances` | Specifies the maximum allowed number of instances per gateway. | Integer | 8 | -| `deployment.minInstances` | Specifies the minimum allowed number of instances per gateway. | Integer | 1 | -| `image.consulAPIGateway` | The image to use for consul-api-gateway. View available image tags on [DockerHub](https://hub.docker.com/r/hashicorp/consul-api-gateway/tags). | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | -| `image.envoy` | Specifies the container image to use for Envoy. View available image tags on [DockerHub](https://hub.docker.com/r/envoyproxy/envoy/tags). | String | `"envoyproxy/envoy:RELEASE_VERSION"` | -| `logLevel` | Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. | String | `"info"` | -| `nodeSelector` | Specifies a set of parameters that constrain the nodes on which the pod can run. Defining nodes with the `nodeSelector` enables the pod to fit on a node. The selector must match a node's labels for the pod to be scheduled on that node. Refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) for additional information. | Object | N/A | -| `serviceType` | Specifies the ingress methods for a service. The following values are supported:
`ClusterIP`
`NodePort`
`LoadBalancer`. | String | N/A | -| `useHostPorts` | If set to `true`, then the Envoy container ports are mapped to host ports. | Boolean | `false` | + + + | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | + | String | `"info"` | +| `nodeSelector` | | Object | N/A | +| `serviceType` | | String | N/A | +| `useHostPorts` | | Boolean | `false` | Refer to the [Consul API Gateway repository](https://github.com/hashicorp/consul-api-gateway/blob/main/config/crd/bases/api-gateway.consul.hashicorp.com_gatewayclassconfigs.yaml) for the complete specification. From fd9c4f7f6d620842c305b022368f4717643d258e Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Tue, 26 Jul 2022 09:30:38 -0500 Subject: [PATCH 055/339] Update website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/api-gateway/configuration/gatewayclassconfig.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 5b35c04aa7..33a7c5cad7 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -11,7 +11,7 @@ This topic provides full details about the `GatewayClassConfig` resource ## Introduction -The `GatewayClassConfig` object describes Consul API Gateway-related configuration parameters for the [`GatewayClass`](#gatewayclass). +The `GatewayClassConfig` object contains Consul API Gateway-related configuration parameters. Apply the parameters by adding the [`GatewayClass`](#gatewayclass) object to your Kubernetes values file and specifying the name of the `GatewayClassConfig`. A `GatewayClassConfig` object provides configuration options specifc to Consul API Gateway From 8baaf2492a69ff9b1bc1fdf440680dc4effdfc65 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 09:31:27 -0500 Subject: [PATCH 056/339] clean up extra sentence --- .../docs/api-gateway/configuration/gatewayclassconfig.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 33a7c5cad7..2abe3da45c 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -11,9 +11,7 @@ This topic provides full details about the `GatewayClassConfig` resource ## Introduction -The `GatewayClassConfig` object contains Consul API Gateway-related configuration parameters. Apply the parameters by adding the [`GatewayClass`](#gatewayclass) object to your Kubernetes values file and specifying the name of the `GatewayClassConfig`. - -A `GatewayClassConfig` object provides configuration options specifc to Consul API Gateway +The `GatewayClassConfig` object contains Consul API Gateway-related configuration parameters. Apply the parameters by adding the [`GatewayClass`](#gatewayclass) object to your Kubernetes values file and specifying the name of the `GatewayClassConfig`. ## Configuration model From 64da99a927ebc5b065da72a5f707e02906b8450e Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 09:33:15 -0500 Subject: [PATCH 057/339] clean up leftover table lines --- .../api-gateway/configuration/gatewayclassconfig.mdx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 2abe3da45c..8a321be388 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -175,13 +175,4 @@ The following example creates a gateway class configuration called `test-gateway ```
- - - - | String | `"hashicorp/consul-api-gateway:RELEASE_VERSION"` | - | String | `"info"` | -| `nodeSelector` | | Object | N/A | -| `serviceType` | | String | N/A | -| `useHostPorts` | | Boolean | `false` | - Refer to the [Consul API Gateway repository](https://github.com/hashicorp/consul-api-gateway/blob/main/config/crd/bases/api-gateway.consul.hashicorp.com_gatewayclassconfigs.yaml) for the complete specification. From b69c3fb410770b59d5ef0f36febe6bfe517981f4 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 10:10:17 -0500 Subject: [PATCH 058/339] restructure allowed values to be consistent --- .../api-gateway/configuration/gateway.mdx | 3 +-- .../configuration/gatewayclass.mdx | 14 ++++++++---- .../configuration/gatewayclassconfig.mdx | 22 +++++++++++++++++-- .../docs/api-gateway/configuration/index.mdx | 6 +++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index b340d3c329..d30123e09c 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -56,13 +56,12 @@ Specify the following parameters to declare a Gateway: This topic provides details about the configuration parameters. ### gatewayClassName -Specifies the name of the [`GatewayClass`](/docs/api-gateway/configuration/gatewayclass) resource used for the `Gateway` instance. +Specifies the name of the [`GatewayClass`](/docs/api-gateway/configuration/gatewayclass) resource used for the `Gateway` instance. Unless a custom [GatewayClass](/docs/api-gateway/configuration/gatewayclass) is being used, value should be set to `consul-api-gateway` * Type: string * Required: required ### listeners Specifies the `listeners` associated with the `Gateway`. At least one `listener` must be specified. Each `listener` within a `Gateway` must have a unique combination of `hostname`, `port`, and `protocol`. - * Type: array of objects * Required: required diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 2a5104f518..8f25467598 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -43,17 +43,23 @@ An object that defines additional configuration required by the gateway controll * Required: required ### parametersRef.group -When using Consul API Gateway, this value should be equal to `api-gateway.consul.hashicorp.com` +The Kubernetes group of the `parametersRef`. This value will always be the same across all deployments of Consul API Gateway. * Type: Group * Required: required +You must specify the following value: +* `api-gateway.consul.hashicorp.com` + ### parametersRef.kind -When using Consul API Gateway, this value should be equal to `GatewayClassConfig` -* Type: object +The Kubernetes kind of the `parametersRef`. This value will always be the same across all deployments of Consul API Gateway. +* Type: Kind * Required: required +You must specify the following value: +* `GatewayClassConfig` + ### parametersRef.name -The name of the `GatewayClass` +The name of the `GatewayClassConfig` object * Type: object * Required: required diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 8a321be388..0eef0d15a6 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -93,6 +93,10 @@ Specifies the scheme to use for connecting to Consul. The supported values are ` * Required: optional * Default: `http` +You can specify the following strings: +* `http` +* `https` + ### copyAnnotations.service List of kubernetes (annotations)[https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/] to copy to the gateway service. * Type: Array of Strings @@ -133,20 +137,34 @@ Most deployments will use the default value unless a specific version of Envoy i * Default: `"envoyproxy/envoy:RELEASE_VERSION"` ### logLevel -Specifies the error reporting level for logs. You can specify the following values: `error`, `warning`, `info`, `debug`, `trace`. +Specifies the error reporting level for logs. * Type: string * Required: optional * Default: `info` +You can specify the following strings: +* `error` +* `warning` +* `info` +* `debug` +* `trace` + ### nodeSelector Specifies a set of parameters that constrain the nodes on which the pod can run. Defining nodes with the `nodeSelector` enables the pod to fit on a node. The selector must match a node's labels for the pod to be scheduled on that node. Refer to the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) for additional information.* Type: string * Required: optional ### serviceType -Specifies the ingress methods for a service. The following values are supported:
`ClusterIP`
`NodePort`
`LoadBalancer`. +Specifies the ingress methods for a service. * Type: string * Required: optional +You can specify the following strings: +* `ClusterIP`: Gateway will only be accessible from inside the cluster +* `NodePort`: Gateway will be exposed on each Kubernetes node at a static port +* `LoadBalancer`: Gateway will be exposed to external traffic by a load balancer + +For more on Kubernetes services, see [Publishing Services](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types) + ### useHostPorts If set to `true`, then the Envoy container ports are mapped to host ports. * Type: boolean diff --git a/website/content/docs/api-gateway/configuration/index.mdx b/website/content/docs/api-gateway/configuration/index.mdx index 8952a0dc4a..32343b079d 100644 --- a/website/content/docs/api-gateway/configuration/index.mdx +++ b/website/content/docs/api-gateway/configuration/index.mdx @@ -14,4 +14,10 @@ This topic provides an overview of the configuration items that enable Consul AP - [GatewayClass](/docs/api-gateway/configuration/gatewayclass): Defines a class of gateway resources that you can use as a template for creating gateways. - [Routes](/docs/api-gateway/configuration/routes): Specifies the path from the gateway to the backend service(s)client to the listener. +A basic Gateway object can simply use the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway###gatewayClassName) `consul-api-gateway`. For additional configruation options follow the following steps. + +1. Define a [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) containing your desired configurations. +1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) with [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass###parametersRef.name) set to the name of the newly defined [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) +1. Define a [Gateway](/docs/api-gateway/configuration/gateway) with [`gatewayClassName`](/docs/api-gateway/configuration/gateway###gatewayClassName) set to the name of the newly defined [GatewayClass](/docs/api-gateway/configuration/gatewayclass) + From e551ac878f4a288bbbbda6011ea9d2942a636944 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 10:25:48 -0500 Subject: [PATCH 059/339] fix links --- .../api-gateway/configuration/gateway.mdx | 42 ++++++++-------- .../configuration/gatewayclass.mdx | 10 ++-- .../configuration/gatewayclassconfig.mdx | 48 +++++++++---------- .../docs/api-gateway/configuration/index.mdx | 8 ++-- .../docs/api-gateway/configuration/routes.mdx | 40 ++++++++-------- 5 files changed, 74 insertions(+), 74 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index d30123e09c..e1c8400a66 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -29,27 +29,27 @@ Specify the following parameters to declare a Gateway: ## Configuration model -* (`gatewayClassName`)[###gatewayClassName]: string | required -* (`listeners`)[###listeners]: array of objects | required - * (`allowedRoutes`)[###listeners.allowedRoutes]: object | required - * (`namespaces`}[###listeners.namespaces]: object | required - * (`from`)[###listeners.namespaces.from]: string | required - * (`selector`)[###listeners.namespaces.selector]: object | required if from is configured to selector - * (`matchExpressions`)[###listeners.namespaces.selector.matchExpressions]: array of objects | required if matchLabels is not configured - * (`key`)[###listeners.namespaces.selector.matchExpressions.key]: string | required if matchExpressions is declared - * (`operator`)[###listeners.namespaces.selector.operator]: string | required if matchExpressions is declared - * (`values`)[###listeners.namespaces.selector.values]: array of strings | required if matchExpressions is declared - * (`matchLabels`)[###listeners.namespaces.selector.matchLabels]: map of strings | required if matchExpressions is not configured - * (`hostname`)[###listeners.hostname]: string | required - * (`name`)[###listeners.name]: string | required - * (`port`)[###listeners.port]: integer | required - * (`protocol)[###listeners.protocol]`: string | required - * (`tls`)[###listeners.tls]: object | required if protocol is set to HTTPS - * (`certificateRefs`)[###listeners.tls.certificateRefs]: array or objects | required if tls is declared - * (`name`)[###listeners.tls.certificateRefs.name]: string | required if certificateRefs is declared - * (`namespace`)[###listeners.tls.certificateRefs.namespace]: string | required if certificateRefs is declared - * (`mode`)[###listeners.tls.mode]: string | required if certificateRefs is declared - * (`options`)[###listeners.tls.options]: map of strings | optional +* (`gatewayClassName`)[#gatewayClassName]: string | required +* (`listeners`)[#listeners]: array of objects | required + * (`allowedRoutes`)[#listeners-allowedRoutes]: object | required + * (`namespaces`}[#listeners-namespaces]: object | required + * (`from`)[#listeners-namespaces-from]: string | required + * (`selector`)[#listeners-namespaces-selector]: object | required if from is configured to selector + * (`matchExpressions`)[#listeners-namespaces-selector-matchExpressions]: array of objects | required if matchLabels is not configured + * (`key`)[#listeners-namespaces-selector-matchExpressions-key]: string | required if matchExpressions is declared + * (`operator`)[#listeners.namespaces-selector-operator]: string | required if matchExpressions is declared + * (`values`)[#listeners.namespaces-selector-values]: array of strings | required if matchExpressions is declared + * (`matchLabels`)[#listeners-namespaces-selector-matchLabels]: map of strings | required if matchExpressions is not configured + * (`hostname`)[#listeners-hostname]: string | required + * (`name`)[#listeners-name]: string | required + * (`port`)[#listeners-port]: integer | required + * (`protocol`)[#listeners-protocol]`: string | required + * (`tls`)[#listeners-tls]: object | required if protocol is set to HTTPS + * (`certificateRefs`)[#listeners-tls-certificateRefs]: array or objects | required if tls is declared + * (`name`)[#listeners-tls-certificateRefs-name]: string | required if certificateRefs is declared + * (`namespace`)[#listeners-tls-certificateRefs-namespace]: string | required if certificateRefs is declared + * (`mode`)[#listeners-tls-mode]: string | required if certificateRefs is declared + * (`options`)[#listeners-tls-options]: map of strings | optional ## Specification diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 8f25467598..0a02c5616e 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -21,11 +21,11 @@ The `GatewayClass` resource is a generic kubernetes gateway object. For configur ## Configuration model -* (`controllerName`)[###controllerName]: string | required -* (`parametersRef`)[###parametersRef]: object | optional - * (`group`)[###parametersRef.group]: Group | required is parametersRef is set - * (`kind`)[###parametersRef.kind]: Kind | required is parametersRef is set - * (`name`)[###parametersRef.name]: string | required is parametersRef is set +* (`controllerName`)[#controllerName]: string | required +* (`parametersRef`)[#parametersRef]: object | optional + * (`group`)[#parametersRef-group]: Group | required is parametersRef is set + * (`kind`)[#parametersRef-kind]: Kind | required is parametersRef is set + * (`name`)[#parametersRef-name]: string | required is parametersRef is set * (`description`)[###description]: string | optional ## Specification diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 0eef0d15a6..6bb669472b 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -15,30 +15,30 @@ The `GatewayClassConfig` object contains Consul API Gateway-related configuratio ## Configuration model -* (`consul`)[###consul]: object | optional - * (`address`)[###consul.address] : string | optional - * (`authentication`)[###consul.authentication]: object | optional - * (`account`)[[###consul.authentication.account] : string | optional - * (`managed`)[###consul.authentication.managed] : bool | optional - * (`method`)[###consul.authentication.method] : string | optional - * (`namespace`)[###consul.authentication.namespace] : string | optional - * (`ports`)[###consul.ports] : object | optional - * (`grpc`)[###consul.ports.grpc] : integer | optional - * (`http`)[###consul.ports.http] : integer | optional - * (`scheme`)[###consul.scheme] : string | optional -* (`copyAnnotations`)[###copyAnnotations] : object | optional - * (`service`)[###copyAnnotations.service] : array of strings | optional -* (`deployment`)[###deployment] : object | optional - * (`defaultInstances`)[###deployment.defaultInstances] : integer | optional - * (`maxInstances`)[###deployment.maxInstances] : integer | optional - * (`minInstances`)[###deployment.minInstances] : integer | optional -* (`image`)[###image] : object | optional - * (`consulAPIGateway`)[###image.consulAPIGateway] : string | optional - * (`envoy`)[###image.envoy] : string | optional -* (`logLevel`)[###logLevel] : string | optional -* (`nodeSelector`)[###nodeSelector] : string | optional -* (`serviceType`)[###serviceType] : string | optional -* (`useHostPorts`)[###useHostPorts] : boolean | optional +* (`consul`)[#consul]: object | optional + * (`address`)[#consul-address] : string | optional + * (`authentication`)[#consul-authentication]: object | optional + * (`account`)[[#consul-authentication-account] : string | optional + * (`managed`)[#consul-authentication-managed] : bool | optional + * (`method`)[#consul-authentication-method] : string | optional + * (`namespace`)[#consul-authentication-namespace] : string | optional + * (`ports`)[#consul-ports] : object | optional + * (`grpc`)[#consul-ports-grpc] : integer | optional + * (`http`)[#consul-ports-http] : integer | optional + * (`scheme`)[#consul-scheme] : string | optional +* (`copyAnnotations`)[#copyAnnotations] : object | optional + * (`service`)[#copyAnnotations-service] : array of strings | optional +* (`deployment`)[#deployment] : object | optional + * (`defaultInstances`)[#deployment-defaultInstances] : integer | optional + * (`maxInstances`)[#deployment-maxInstances] : integer | optional + * (`minInstances`)[#deployment-minInstances] : integer | optional +* (`image`)[#image] : object | optional + * (`consulAPIGateway`)[#image-consulAPIGateway] : string | optional + * (`envoy`)[#image-envoy] : string | optional +* (`logLevel`)[#logLevel] : string | optional +* (`nodeSelector`)[#nodeSelector] : string | optional +* (`serviceType`)[#serviceType] : string | optional +* (`useHostPorts`)[#useHostPorts] : boolean | optional ## Specification diff --git a/website/content/docs/api-gateway/configuration/index.mdx b/website/content/docs/api-gateway/configuration/index.mdx index 32343b079d..9c5606a404 100644 --- a/website/content/docs/api-gateway/configuration/index.mdx +++ b/website/content/docs/api-gateway/configuration/index.mdx @@ -9,15 +9,15 @@ description: >- This topic provides an overview of the configuration items that enable Consul API Gateway to facilitate ingress into your Consul service mesh. -- [Gateway](/docs/api-gateway/configuration/gateway): Defines the main infrastructure resource that links API gateway components. It specifies the name of the `GatewayClass` and one or more listeners (see [Listeners](/docs/api-gateway/configuration/gateway#listeners)), which specify the logical endpoints bound to the gateway's addresses. Refer to Configuration > Gateway for details on configuration. +- [Gateway](/docs/api-gateway/configuration/gateway): Defines the main infrastructure resource that links API gateway components. It specifies the name of the `GatewayClass` and one or more listeners (see [Listeners](/docs/api-gateway/configuration/gateway#listeners)), which specify the logical endpoints bound to the gateway's addresses. - [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig): Describes additional Consul API Gateway-related configuration parameters for the GatewayClass resource. - [GatewayClass](/docs/api-gateway/configuration/gatewayclass): Defines a class of gateway resources that you can use as a template for creating gateways. - [Routes](/docs/api-gateway/configuration/routes): Specifies the path from the gateway to the backend service(s)client to the listener. -A basic Gateway object can simply use the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway###gatewayClassName) `consul-api-gateway`. For additional configruation options follow the following steps. +A basic Gateway object can simply use the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayClassName) `consul-api-gateway`. For additional configruation options follow the following steps. 1. Define a [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) containing your desired configurations. -1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) with [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass###parametersRef.name) set to the name of the newly defined [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) -1. Define a [Gateway](/docs/api-gateway/configuration/gateway) with [`gatewayClassName`](/docs/api-gateway/configuration/gateway###gatewayClassName) set to the name of the newly defined [GatewayClass](/docs/api-gateway/configuration/gatewayclass) +1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) with [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass#parametersRef-name) set to the name of the newly defined [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) +1. Define a [Gateway](/docs/api-gateway/configuration/gateway) with [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayClassName) set to the name of the newly defined [GatewayClass](/docs/api-gateway/configuration/gatewayclass) diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index 13e720f56e..7819d482f6 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -47,33 +47,33 @@ The following example creates a route named `example-route` in namespace `gatewa apiVersion: gateway.networking.k8s.io/v1alpha2 kind: HTTPRoute metadata: - name: example-route - namespace: gateway-namespace + name: example-route + namespace: gateway-namespace spec: - parentRefs: - - name: example-gateway - rules: - - backendRefs: - - kind: Service - name: echo - namespace: service-namespace - port: 8080 + parentRefs: + - name: example-gateway + rules: + - backendRefs: + - kind: Service + name: echo + namespace: service-namespace + port: 8080 --- apiVersion: gateway.networking.k8s.io/v1alpha2 kind: ReferencePolicy metadata: - name: reference-policy - namespace: service-namespace + name: reference-policy + namespace: service-namespace spec: - from: - - group: gateway.networking.k8s.io - kind: HTTPRoute - namespace: gateway-namespace - to: - - group: "" - kind: Service - name: echo + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: gateway-namespace + to: + - group: "" + kind: Service + name: echo ``` From b2cf7f0d3defdeaf12f4c2cd374618a7fade2638 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 12:34:10 -0500 Subject: [PATCH 060/339] fix links --- .../api-gateway/configuration/gateway.mdx | 42 ++++++++-------- .../configuration/gatewayclass.mdx | 12 ++--- .../configuration/gatewayclassconfig.mdx | 50 +++++++++---------- .../docs/api-gateway/configuration/index.mdx | 6 +-- 4 files changed, 55 insertions(+), 55 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index e1c8400a66..135d22f9f2 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -29,27 +29,27 @@ Specify the following parameters to declare a Gateway: ## Configuration model -* (`gatewayClassName`)[#gatewayClassName]: string | required -* (`listeners`)[#listeners]: array of objects | required - * (`allowedRoutes`)[#listeners-allowedRoutes]: object | required - * (`namespaces`}[#listeners-namespaces]: object | required - * (`from`)[#listeners-namespaces-from]: string | required - * (`selector`)[#listeners-namespaces-selector]: object | required if from is configured to selector - * (`matchExpressions`)[#listeners-namespaces-selector-matchExpressions]: array of objects | required if matchLabels is not configured - * (`key`)[#listeners-namespaces-selector-matchExpressions-key]: string | required if matchExpressions is declared - * (`operator`)[#listeners.namespaces-selector-operator]: string | required if matchExpressions is declared - * (`values`)[#listeners.namespaces-selector-values]: array of strings | required if matchExpressions is declared - * (`matchLabels`)[#listeners-namespaces-selector-matchLabels]: map of strings | required if matchExpressions is not configured - * (`hostname`)[#listeners-hostname]: string | required - * (`name`)[#listeners-name]: string | required - * (`port`)[#listeners-port]: integer | required - * (`protocol`)[#listeners-protocol]`: string | required - * (`tls`)[#listeners-tls]: object | required if protocol is set to HTTPS - * (`certificateRefs`)[#listeners-tls-certificateRefs]: array or objects | required if tls is declared - * (`name`)[#listeners-tls-certificateRefs-name]: string | required if certificateRefs is declared - * (`namespace`)[#listeners-tls-certificateRefs-namespace]: string | required if certificateRefs is declared - * (`mode`)[#listeners-tls-mode]: string | required if certificateRefs is declared - * (`options`)[#listeners-tls-options]: map of strings | optional +* [`gatewayClassName`](#gatewayClassName): string | required +* [`listeners`](#listeners): array of objects | required + * [`allowedRoutes`](#listeners-allowedRoutes): object | required + * [`namespaces`](#listeners-namespaces): object | required + * [`from`](#listeners-namespaces-from): string | required + * [`selector`](#listeners-namespaces-selector): object | required if from is configured to selector + * [`matchExpressions`](#listeners-namespaces-selector-matchExpressions): array of objects | required if matchLabels is not configured + * [`key`](#listeners-namespaces-selector-matchExpressions-key): string | required if matchExpressions is declared + * [`operator`](#listeners.namespaces-selector-operator): string | required if matchExpressions is declared + * [`values`](#listeners.namespaces-selector-values): array of strings | required if matchExpressions is declared + * [`matchLabels`](#listeners-namespaces-selector-matchLabels): map of strings | required if matchExpressions is not configured + * [`hostname`](#listeners-hostname): string | required + * [`name`](#listeners-name): string | required + * [`port`](#listeners-port): integer | required + * [`protocol`](#listeners-protocol]`: string | required + * [`tls`](#listeners-tls): object | required if protocol is set to HTTPS + * [`certificateRefs`](#listeners-tls-certificateRefs): array or objects | required if tls is declared + * [`name`](#listeners-tls-certificateRefs-name): string | required if certificateRefs is declared + * [`namespace`](#listeners-tls-certificateRefs-namespace): string | required if certificateRefs is declared + * [`mode`](#listeners-tls-mode): string | required if certificateRefs is declared + * [`options`](#listeners-tls-options): map of strings | optional ## Specification diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 0a02c5616e..0c57ad6c04 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -21,12 +21,12 @@ The `GatewayClass` resource is a generic kubernetes gateway object. For configur ## Configuration model -* (`controllerName`)[#controllerName]: string | required -* (`parametersRef`)[#parametersRef]: object | optional - * (`group`)[#parametersRef-group]: Group | required is parametersRef is set - * (`kind`)[#parametersRef-kind]: Kind | required is parametersRef is set - * (`name`)[#parametersRef-name]: string | required is parametersRef is set -* (`description`)[###description]: string | optional +* [`controllerName`](#controllername): string | required +* [`parametersRef`](#parametersref): object | optional + * [`group`]([#parametersref-group): Group | required is parametersRef is set + * [`kind`](#parametersref-kind): Kind | required is parametersRef is set + * [`name`](#parametersref-name): string | required is parametersRef is set +* [`description`](#description): string | optional ## Specification diff --git a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx index 6bb669472b..e0061e346f 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclassconfig.mdx @@ -15,30 +15,30 @@ The `GatewayClassConfig` object contains Consul API Gateway-related configuratio ## Configuration model -* (`consul`)[#consul]: object | optional - * (`address`)[#consul-address] : string | optional - * (`authentication`)[#consul-authentication]: object | optional - * (`account`)[[#consul-authentication-account] : string | optional - * (`managed`)[#consul-authentication-managed] : bool | optional - * (`method`)[#consul-authentication-method] : string | optional - * (`namespace`)[#consul-authentication-namespace] : string | optional - * (`ports`)[#consul-ports] : object | optional - * (`grpc`)[#consul-ports-grpc] : integer | optional - * (`http`)[#consul-ports-http] : integer | optional - * (`scheme`)[#consul-scheme] : string | optional -* (`copyAnnotations`)[#copyAnnotations] : object | optional - * (`service`)[#copyAnnotations-service] : array of strings | optional -* (`deployment`)[#deployment] : object | optional - * (`defaultInstances`)[#deployment-defaultInstances] : integer | optional - * (`maxInstances`)[#deployment-maxInstances] : integer | optional - * (`minInstances`)[#deployment-minInstances] : integer | optional -* (`image`)[#image] : object | optional - * (`consulAPIGateway`)[#image-consulAPIGateway] : string | optional - * (`envoy`)[#image-envoy] : string | optional -* (`logLevel`)[#logLevel] : string | optional -* (`nodeSelector`)[#nodeSelector] : string | optional -* (`serviceType`)[#serviceType] : string | optional -* (`useHostPorts`)[#useHostPorts] : boolean | optional +* [`consul`](#consul): object | optional + * [`address`](#consul-address): string | optional + * [`authentication`](#consul-authentication): object | optional + * [`account`]([#consul-authentication-account): string | optional + * [`managed`](#consul-authentication-managed): bool | optional + * [`method`](#consul-authentication-method): string | optional + * [`namespace`](#consul-authentication-namespace): string | optional + * [`ports`](#consul-ports): object | optional + * [`grpc`](#consul-ports-grpc): integer | optional + * [`http`](#consul-ports-http): integer | optional + * [`scheme`](#consul-scheme): string | optional +* [`copyAnnotations`](#copyAnnotations): object | optional + * [`service`](#copyAnnotations-service): array of strings | optional +* [`deployment`](#deployment): object | optional + * [`defaultInstances`](#deployment-defaultinstances): integer | optional + * [`maxInstances`](#deployment-maxinstances): integer | optional + * [`minInstances`](#deployment-mininstances): integer | optional +* [`image`](#image): object | optional + * [`consulAPIGateway`](#image-consulapigateway): string | optional + * [`envoy`](#image-envoy): string | optional +* [`logLevel`](#loglevel): string | optional +* [`nodeSelector`](#nodeselector): string | optional +* [`serviceType`](#servicetype): string | optional +* [`useHostPorts`](#usehostports): boolean | optional ## Specification @@ -98,7 +98,7 @@ You can specify the following strings: * `https` ### copyAnnotations.service -List of kubernetes (annotations)[https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/] to copy to the gateway service. +List of kubernetes [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) to copy to the gateway service. * Type: Array of Strings * Required: optional diff --git a/website/content/docs/api-gateway/configuration/index.mdx b/website/content/docs/api-gateway/configuration/index.mdx index 9c5606a404..fd1ac2a9dd 100644 --- a/website/content/docs/api-gateway/configuration/index.mdx +++ b/website/content/docs/api-gateway/configuration/index.mdx @@ -14,10 +14,10 @@ This topic provides an overview of the configuration items that enable Consul AP - [GatewayClass](/docs/api-gateway/configuration/gatewayclass): Defines a class of gateway resources that you can use as a template for creating gateways. - [Routes](/docs/api-gateway/configuration/routes): Specifies the path from the gateway to the backend service(s)client to the listener. -A basic Gateway object can simply use the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayClassName) `consul-api-gateway`. For additional configruation options follow the following steps. +A basic Gateway object can simply use the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayclassname) `consul-api-gateway`. For additional configruation options follow the following steps. 1. Define a [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) containing your desired configurations. -1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) with [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass#parametersRef-name) set to the name of the newly defined [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) -1. Define a [Gateway](/docs/api-gateway/configuration/gateway) with [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayClassName) set to the name of the newly defined [GatewayClass](/docs/api-gateway/configuration/gatewayclass) +1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) with [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass#parametersref-name) set to the name of the newly defined [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) +1. Define a [Gateway](/docs/api-gateway/configuration/gateway) with [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayclassname) set to the name of the newly defined [GatewayClass](/docs/api-gateway/configuration/gatewayclass) From 43e4f09a1633306a25b210091b43291e75c88712 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 12:42:46 -0500 Subject: [PATCH 061/339] update controllername to match previous style --- .../content/docs/api-gateway/configuration/gatewayclass.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 0c57ad6c04..918b872523 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -33,10 +33,13 @@ The `GatewayClass` resource is a generic kubernetes gateway object. For configur This topic provides details about the configuration parameters. ### controllerName -The name of the controller that is managing the gateways of this class. When using Consul API Gateway, this value should be equal to `'hashicorp.com/consul-api-gateway-controller'` +The name of the controller that is managing the gateways of this class. * Type: string * Required: required +You must specify the following value: +* `'hashicorp.com/consul-api-gateway-controller'` + ### parametersRef An object that defines additional configuration required by the gateway controller. * Type: object From 243519c8efe1382cda5b4851570ee38fa895b940 Mon Sep 17 00:00:00 2001 From: Sarah Alsmiller Date: Tue, 26 Jul 2022 12:59:28 -0500 Subject: [PATCH 062/339] fix links --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 135d22f9f2..ea01afa627 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -43,7 +43,7 @@ Specify the following parameters to declare a Gateway: * [`hostname`](#listeners-hostname): string | required * [`name`](#listeners-name): string | required * [`port`](#listeners-port): integer | required - * [`protocol`](#listeners-protocol]`: string | required + * [`protocol`](#listeners-protocol)`: string | required * [`tls`](#listeners-tls): object | required if protocol is set to HTTPS * [`certificateRefs`](#listeners-tls-certificateRefs): array or objects | required if tls is declared * [`name`](#listeners-tls-certificateRefs-name): string | required if certificateRefs is declared From f520f6dd0fa11a4b29d855599b39addc0d428374 Mon Sep 17 00:00:00 2001 From: Sarah Pratt Date: Tue, 26 Jul 2022 13:31:06 -0500 Subject: [PATCH 063/339] Separate port and socket path requirement in case of local agent assignment --- agent/agent_endpoint.go | 8 ++++---- agent/sidecar_service_test.go | 2 +- agent/structs/structs.go | 25 +++++++++++++++++++++---- agent/structs/structs_test.go | 10 ++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 11ec5f9ca7..65d8af1dff 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -1123,9 +1123,9 @@ func (s *HTTPHandlers) AgentRegisterService(resp http.ResponseWriter, req *http. return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Invalid Service Meta: %v", err)} } - // Run validation. This is the same validation that would happen on - // the catalog endpoint so it helps ensure the sync will work properly. - if err := ns.Validate(); err != nil { + // Run validation. This same validation would happen on the catalog endpoint, + // so it helps ensure the sync will work properly. + if err := ns.ValidateForAgent(); err != nil { return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Validation failed: %v", err.Error())} } @@ -1164,7 +1164,7 @@ func (s *HTTPHandlers) AgentRegisterService(resp http.ResponseWriter, req *http. return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Invalid SidecarService: %s", err)} } if sidecar != nil { - if err := sidecar.Validate(); err != nil { + if err := sidecar.ValidateForAgent(); err != nil { return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Failed Validation: %v", err.Error())} } // Make sure we are allowed to register the sidecar using the token diff --git a/agent/sidecar_service_test.go b/agent/sidecar_service_test.go index a2ffe9af49..7ced9720a7 100644 --- a/agent/sidecar_service_test.go +++ b/agent/sidecar_service_test.go @@ -339,7 +339,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { } ns := tt.sd.NodeService() - err := ns.Validate() + err := ns.ValidateForAgent() require.NoError(t, err, "Invalid test case - NodeService must validate") gotNS, gotChecks, gotToken, err := a.sidecarServiceFromNodeService(ns, tt.token) diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 275bf4c18b..7a7bd93f5c 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -1411,6 +1411,27 @@ func (s *NodeService) IsGateway() bool { func (s *NodeService) Validate() error { var result error + if s.Kind == ServiceKindConnectProxy { + if s.Port == 0 && s.SocketPath == "" { + result = multierror.Append(result, fmt.Errorf("Port or SocketPath must be set for a %s", s.Kind)) + } + } + + commonValidation := s.ValidateForAgent() + if commonValidation != nil { + result = multierror.Append(result, commonValidation) + } + + return result +} + +// ValidateForAgent does a subset validation, with the assumption that a local agent can assist with missing values. +// +// I.e. in the catalog case, a local agent cannot be assumed to facilitate auto-assignment of port or socket path, +// so additional checks are needed. +func (s *NodeService) ValidateForAgent() error { + var result error + // TODO(partitions): remember to double check that this doesn't cross partition boundaries // ConnectProxy validation @@ -1426,10 +1447,6 @@ func (s *NodeService) Validate() error { "services")) } - if s.Port == 0 && s.SocketPath == "" { - result = multierror.Append(result, fmt.Errorf("Port or SocketPath must be set for a %s", s.Kind)) - } - if s.Connect.Native { result = multierror.Append(result, fmt.Errorf( "A Proxy cannot also be Connect Native, only typical services")) diff --git a/agent/structs/structs_test.go b/agent/structs/structs_test.go index 0b6efb3309..844189c2f8 100644 --- a/agent/structs/structs_test.go +++ b/agent/structs/structs_test.go @@ -1157,6 +1157,16 @@ func TestStructs_NodeService_ValidateConnectProxy(t *testing.T) { } } +func TestStructs_NodeService_ValidateConnectProxyWithAgentAutoAssign(t *testing.T) { + t.Run("connect-proxy: no port set", func(t *testing.T) { + ns := TestNodeServiceProxy(t) + ns.Port = 0 + + err := ns.ValidateForAgent() + assert.True(t, err == nil) + }) +} + func TestStructs_NodeService_ValidateConnectProxy_In_Partition(t *testing.T) { cases := []struct { Name string From 5dad74df3c80ad262683a91e957c779e1c3ea5f6 Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Wed, 27 Jul 2022 15:27:04 -0700 Subject: [PATCH 064/339] fixed links and clarified some sections in gateway configuration doc --- .../api-gateway/configuration/gateway.mdx | 123 ++++++++++-------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index ea01afa627..61b36b65eb 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -2,7 +2,7 @@ layout: docs page_title: Consul API Gateway Gateway description: >- - Consul API Gateway Gateway + This topic descrbes how to configure the Consul API Gateway Gateway object --- # Gateway @@ -22,34 +22,36 @@ Specify the following parameters to declare a Gateway: | `kind` | Specifies the type of configuration object. The value should always be `Gateway`. | Required | | `description` | Human-readable string that describes the purpose of the `Gateway`. | Optional | | `version ` | Specifies the Kubernetes API version. The value should always be `gateway.networking.k8s.io/v1alpha2` | Required | -| `scope` | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | -| `fields` | Specifies the configurations for the Gateway. The fields are listed in the Configuration model. Details for each field are described in the Specification. | Required | +| `scope` | Specifies the effective scope of the Gateway. The value should always be `namespaced`. | Required | +| `fields` | Specifies the configurations for the Gateway. The fields are listed in the [configuration model](#configuration-model). Details for each field are described in the [specification](#specification). | Required | ## Configuration model -* [`gatewayClassName`](#gatewayClassName): string | required +The following outline shows how to format the configurations in the `Gateway` object. Click on a property name to view details about the configuration. + +* [`gatewayClassName`](#gatewayclassname): string | required * [`listeners`](#listeners): array of objects | required - * [`allowedRoutes`](#listeners-allowedRoutes): object | required - * [`namespaces`](#listeners-namespaces): object | required - * [`from`](#listeners-namespaces-from): string | required - * [`selector`](#listeners-namespaces-selector): object | required if from is configured to selector - * [`matchExpressions`](#listeners-namespaces-selector-matchExpressions): array of objects | required if matchLabels is not configured - * [`key`](#listeners-namespaces-selector-matchExpressions-key): string | required if matchExpressions is declared - * [`operator`](#listeners.namespaces-selector-operator): string | required if matchExpressions is declared - * [`values`](#listeners.namespaces-selector-values): array of strings | required if matchExpressions is declared - * [`matchLabels`](#listeners-namespaces-selector-matchLabels): map of strings | required if matchExpressions is not configured + * [`allowedRoutes`](#listeners-allowedroutes): object | required + * [`namespaces`](#listeners-allowedroutes-namespaces): object | required + * [`from`](#listeners-namespaces-from): string | required + * [`selector`](#listeners-allowedroutes-namespaces-selector): object | required if `from` is configured to `selector` + * [`matchExpressions`](#listeners-allowedroutes-namespaces-selector-matchexpressions): array of objects | required if `matchLabels` is not configured + * [`key`](#listeners-allowedroutes-namespaces-selector-matchexpressions): string | required if `matchExpressions` is declared + * [`operator`](#listeners-allowedroutes-namespaces-selector-matchexpressions): string | required if `matchExpressions` is declared + * [`values`](#listeners-allowedroutes-namespaces-selector-matchexpressions): array of strings | required if `matchExpressions` is declared + * [`matchLabels`](#listeners-allowedroutes-namespaces-selector-matchlabels): map of strings | required if `matchExpressions` is not configured * [`hostname`](#listeners-hostname): string | required * [`name`](#listeners-name): string | required * [`port`](#listeners-port): integer | required * [`protocol`](#listeners-protocol)`: string | required - * [`tls`](#listeners-tls): object | required if protocol is set to HTTPS - * [`certificateRefs`](#listeners-tls-certificateRefs): array or objects | required if tls is declared - * [`name`](#listeners-tls-certificateRefs-name): string | required if certificateRefs is declared - * [`namespace`](#listeners-tls-certificateRefs-namespace): string | required if certificateRefs is declared - * [`mode`](#listeners-tls-mode): string | required if certificateRefs is declared - * [`options`](#listeners-tls-options): map of strings | optional + * [`tls`](#listeners-tls): object | required if `protocol` is set to `HTTPS` + * [`certificateRefs`](#listeners-tls): array or objects | required if `tls` is declared + * [`name`](#listeners-tls): string | required if `certificateRefs` is declared + * [`namespace`](#listeners-tls): string | required if `certificateRefs` is declared + * [`mode`](#listeners-tls): string | required if `certificateRefs` is declared + * [`options`](#listeners-tls): map of strings | optional ## Specification @@ -74,37 +76,34 @@ Specifies a `namespace` object that defines the types of routes that may be atta Determines which routes are allowed to attach to the `listener`. Only routes in the same namespace as the `Gateway` may be attached by default. ### listeners.allowedRoutes.namespaces.from -Specifies the policy for which namespaces a route may attach to a `Gateway` from. Defaults to `Same`. +Determines which namespaces are allowed to attach a route to the `Gateway`. You can specify one of the following strings: -This parameter has the following properties: -* Type: string -* Required: required - -You can specify one of the following strings: * `All`: Routes in all namespaces may be attached to the `Gateway`. -* `Same`: Only routes in the same namespace as the `Gateway` may be attached. -* `Selector`: Only routes in namespaces that match the `selector` may be attached. +* `Same` (default): Only routes in the same namespace as the `Gateway` may be attached. +* `Selector`: Only routes in namespaces that match the [`selector`](#listeners-allowedroutes-namespaces-selector) may be attached. + +This parameter is required. ### listeners.allowedRoutes.namespaces.selector -Specifies a method of matching namespaces from which routes are allowed to attach to the listener. -* Type: Object -* Required: Required when `from` is configured to `Selector`. +Specifies a method for selecting routes that are allowed to attach to the listener. The `Gateway` checks for namespaces in the network that match either a regular expression or a label. Routes from the matching namespace are allowed to attach to the listener. -The selector configuration contains one of the following objects: -* `matchExpressions` -* `matchLabels` +You can configure one of the following objects: + +* [`matchExpressions`](#listeners-allowedroutes-namespaces-selector-matchexpressions) +* [`matchLabels`](#listeners-allowedroutes-namespaces-selector-matchlabels) + +This field is required when [`from`](#listeners-allowedroutes-namespaces-from) is configured to `Selector`. ### listeners.allowedRoutes.namespaces.selector.matchExpressions Specifies an array of requirements for matching namespaces. If a match is found, then routes from the matching namespace(s) are allowed to attach to the `Gateway`. The following table describes members of the `matchExpressions` array: -| Requirement | Description | Type | -|:----------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |:---------------- | -|`key` | Specifies that label that the key applies to. | string | -|`operator` | Specifies the key's relation to a set of values. The following values are valid:In: description of what this means NotIn: description of what this means Exists: description of what this means DoesNotExist: description of what this means | string | -|`values` | Specifies an array of string values. If the operator is configured to In or NotIn,the values array must be non-empty. If theoperator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. | array of strings | -|`scope` | Specifies the effective scope of the Gateway. The value should always be namespaced. | Required | -|`fields` | Specifies the configurations for the Gateway. The fields are listed in the Configuration model. Details for each field are described in the Specification. | Required | +| Requirement | Description | Type | Required | +|--- |--- |--- |--- | +|`key` | Specifies the label that the `key` applies to. | string | required when `matchExpressions` is declared | +|`operator` | Specifies the key's relation to a set of values. You can use the following keywords:
  • `In`: Only routes in namespaces that contain the strings in the `values` field can attach to the `Gateway`.
  • `NotIn`: Routes in namespaces that do not contain the strings in the `values` field can attach to the `Gateway`.
  • `Exists`: Routes in namespaces that contain the `key` value are allowed to attach to the `Gateway`.
  • `DoesNotExist`: Routes in namespaces that do not contain the `key` value are allowed to attach to the `Gateway`.
| string | required when `matchExpressions` is declared | +|`values` | Specifies an array of string values. If `operator` is configured to `In` or `NotIn`, then the `values` array must contain values. If `operator` is configured to `Exists` or `DoesNotExist`, then the `values` array must be empty. This array is replaced during a strategic merge patch. | array of strings | required when `matchExpressions` is declared | +In the following example, routes in namespaces that contain `foo` and `bar` are allowed to attach routes to the `Gateway`. ```yaml namespaceSelector: matchExpressions: @@ -118,52 +117,62 @@ namespaceSelector: ### listeners.allowedRoutes.namespaces.selector.matchLabels Specifies an array of labels and label values. If a match is found, then routes with the matching label(s) are allowed to attach to the `Gateway`. This selector can contain any arbitrary key/value pair. +In the following example, routes in namespaces that have a `bar` label are allowed to attache to the `Gateway`. ```yaml namespaceSelector: matchLabels: foo: bar ``` -For more on labels, see [Labels and Selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) +Refer to [Labels and Selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) in the Kubernetes documentation for additional information about labels. ### listeners.hostname -Specifies the `listener`'s hostname +Specifies the `listener`'s hostname. * Type: string * Required: required ### listeners.name -Specifies the `listener`'s name +Specifies the `listener`'s name. * Type: string * Required: required ### listeners.port -Specifies the port number that the `listener` will attach to +Specifies the port number that the `listener` attaches to. * Type: integer * Required: required ### listeners.protocol -Specifies the protocol the `listener` will use +Specifies the protocol the `listener` communicates on. * Type: string * Required: required Allowed values are `TCP`, `HTTP`, or `HTTPS` ### listeners.tls -* Type: Object -* Required: required if `protocol` is set to `HTTPS` +Specifies the `tls` configurations for the `Gateway`. The `tls` object is required if `protocol` is set to `HTTPS`. The object contains the following fields: -### listeners.tls.certificateRefs -`CertificateRefs` contains a series of references to Kubernetes objects that contains TLS certificates and private keys. These certificates are used to establish a TLS handshake for requests that match the hostname of the associated `listener`. Each reference must be a Kubernetes Secret, and, if using a Secret in a namespace other than the`Gateway`'s, must have a corresponding `ReferencePolicy` created. -* Type: Object or Array -* Required: required if `tls` is set +| Parameter | Description | Type | Required | +| --- | --- | --- | --- | +| `certificateRefs` |
Specifies Kubernetes `name` and `namespace` objects that contains TLS certificates and private keys.
The certificates establish a TLS handshake for requests that match the `hostname` of the associated `listener`. Each reference must be a Kubernetes Secret. If you are using a Secret in a namespace other than the `Gateway`'s, each reference must also have a corresponding [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy).
| Object or array | Required if `tls` is set | +| `mode` | Specifies the ? | string | Required if `certificateRefs` is set | +| `options` | ??? | Map of strings | optional | -### listeners.tls.mode -* Type: String -* Required: required if certificateRefs is set +In the following example, `tls` settings are configured . . . + +```yaml + +tls: + certificateRefs: + name: ? + namespace: ? + mode: ? + options: + - ? + - ? + - ? + +``` -### listeners.tls.options -* Type: Map of Strings -* Required: optional ## Complete configuration The following example shows a fully configured `Gateway`. From 62003637df2975c6d15d37e1b18049bc231c5fd2 Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Wed, 27 Jul 2022 15:32:26 -0700 Subject: [PATCH 065/339] fixed small typo --- website/content/docs/api-gateway/configuration/gateway.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 61b36b65eb..8907b753e1 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -117,7 +117,8 @@ namespaceSelector: ### listeners.allowedRoutes.namespaces.selector.matchLabels Specifies an array of labels and label values. If a match is found, then routes with the matching label(s) are allowed to attach to the `Gateway`. This selector can contain any arbitrary key/value pair. -In the following example, routes in namespaces that have a `bar` label are allowed to attache to the `Gateway`. +In the following example, routes in namespaces that have a `bar` label are allowed to attach to the `Gateway`. + ```yaml namespaceSelector: matchLabels: From 176a56839ead54867e52d90a7740511417b9ae56 Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Wed, 27 Jul 2022 16:08:54 -0700 Subject: [PATCH 066/339] minor changes to the gatewayclass documentation --- .../configuration/gatewayclass.mdx | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index 918b872523..f231fed999 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -17,15 +17,16 @@ The value of the `controllerName` field must be set to `hashicorp.com/consul-api When gateways are created from a `GatewayClass`, they use the parameters specified in the `GatewayClass` at the time of instantiation. -The `GatewayClass` resource is a generic kubernetes gateway object. For configuration specific to Consul API Gateway, see [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig). +The `GatewayClass` resource is a generic Kubernetes gateway object. For configuration specific to Consul API Gateway, see [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig). ## Configuration model +The following outline shows how to format the configurations in the `GatewayClass` object. Click on a property name to view details about the configuration. * [`controllerName`](#controllername): string | required * [`parametersRef`](#parametersref): object | optional - * [`group`]([#parametersref-group): Group | required is parametersRef is set - * [`kind`](#parametersref-kind): Kind | required is parametersRef is set - * [`name`](#parametersref-name): string | required is parametersRef is set + * [`group`]([#parametersref-group): string | required is `parametersRef` is set + * [`kind`](#parametersref-kind): string | required is `parametersRef` is set + * [`name`](#parametersref-name): string | required is `parametersRef` is set * [`description`](#description): string | optional ## Specification @@ -33,41 +34,39 @@ The `GatewayClass` resource is a generic kubernetes gateway object. For configur This topic provides details about the configuration parameters. ### controllerName -The name of the controller that is managing the gateways of this class. +Specifies the name of the controller that manages the gateways generated by this class. The value must alwasy be `'hashicorp.com/consul-api-gateway-controller'`. + * Type: string * Required: required -You must specify the following value: -* `'hashicorp.com/consul-api-gateway-controller'` - ### parametersRef -An object that defines additional configuration required by the gateway controller. +Defines an object that specifies additional configurations required by the gateway controller. * Type: object * Required: required ### parametersRef.group -The Kubernetes group of the `parametersRef`. This value will always be the same across all deployments of Consul API Gateway. -* Type: Group -* Required: required +Specifies the Kubernetes group that the `parametersRef` is a member of. The value must always be `api-gateway.consul.hashicorp.com`. -You must specify the following value: -* `api-gateway.consul.hashicorp.com` +The `parametersRef.group` is always the same across all deployments of Consul API Gateway. + +* Type: string +* Required: required ### parametersRef.kind -The Kubernetes kind of the `parametersRef`. This value will always be the same across all deployments of Consul API Gateway. -* Type: Kind +Specifies the Kubernetes kind of entity that the `parametersRef` is. The value must always be `GatewayClassConfig`. + +This `parametersRef.kind` is always the same across all deployments of Consul API Gateway. + +* Type: string * Required: required -You must specify the following value: -* `GatewayClassConfig` - ### parametersRef.name -The name of the `GatewayClassConfig` object +Specfies the name of the `GatewayClassConfig` object. * Type: object * Required: required ### description -Helps describe a gateway class with more details +Specifies a human-readable description of the gateway class. We recommend including a description so that a record exists that describes the gateway class's purpose. * Type: string * Required: optional From a3ef6f016e9af3b8c3ebc7d29fbc0f193b68c23e Mon Sep 17 00:00:00 2001 From: Sarah Pratt Date: Wed, 27 Jul 2022 13:19:17 -0500 Subject: [PATCH 067/339] refactor sidecare_service method into parts --- agent/agent_endpoint.go | 2 +- agent/sidecar_service.go | 81 +++++++++------- agent/sidecar_service_test.go | 176 +++++++++++++++++----------------- 3 files changed, 137 insertions(+), 122 deletions(-) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 65d8af1dff..b2d68e3044 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -1125,7 +1125,7 @@ func (s *HTTPHandlers) AgentRegisterService(resp http.ResponseWriter, req *http. // Run validation. This same validation would happen on the catalog endpoint, // so it helps ensure the sync will work properly. - if err := ns.ValidateForAgent(); err != nil { + if err := ns.Validate(); err != nil { return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Validation failed: %v", err.Error())} } diff --git a/agent/sidecar_service.go b/agent/sidecar_service.go index 673a02252e..ea58a7a676 100644 --- a/agent/sidecar_service.go +++ b/agent/sidecar_service.go @@ -114,9 +114,32 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str } } + port, err := a.sidecarPortFromServiceIDLocked(sidecar.Port, sidecar.CompoundServiceID()) + if err != nil { + return nil, nil, "", err + } + sidecar.Port = port + + // Setup checks + checks, err := ns.Connect.SidecarService.CheckTypes() + if err != nil { + return nil, nil, "", err + } + // Setup default check if none given + if len(checks) < 1 { + checks = sidecarDefaultChecks(ns.ID, sidecar.Proxy.LocalServiceAddress, sidecar.Port) + } + + return sidecar, checks, token, nil +} + +// sidecarPortFromServiceID is used to allocate a unique port for a sidecar proxy. +// This is called immediately before registration to avoid value collisions. This function assumes the state lock is already held. +func (a *Agent) sidecarPortFromServiceIDLocked(sidecarPort int, sidecarCompoundServiceID structs.ServiceID) (int, error) { + // Allocate port if needed (min and max inclusive). rangeLen := a.config.ConnectSidecarMaxPort - a.config.ConnectSidecarMinPort + 1 - if sidecar.Port < 1 && a.config.ConnectSidecarMinPort > 0 && rangeLen > 0 { + if sidecarPort < 1 && a.config.ConnectSidecarMinPort > 0 && rangeLen > 0 { // This did pick at random which was simpler but consul reload would assign // new ports to all the sidecars since it unloads all state and // re-populates. It also made this more difficult to test (have to pin the @@ -130,11 +153,11 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str // Check if other port is in auto-assign range if otherNS.Port >= a.config.ConnectSidecarMinPort && otherNS.Port <= a.config.ConnectSidecarMaxPort { - if otherNS.CompoundServiceID() == sidecar.CompoundServiceID() { + if otherNS.CompoundServiceID() == sidecarCompoundServiceID { // This sidecar is already registered with an auto-port and is just // being updated so pick the same port as before rather than allocate // a new one. - sidecar.Port = otherNS.Port + sidecarPort = otherNS.Port break } usedPorts[otherNS.Port] = struct{}{} @@ -147,54 +170,48 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str // Check we still need to assign a port and didn't find we already had one // allocated. - if sidecar.Port < 1 { + if sidecarPort < 1 { // Iterate until we find lowest unused port for p := a.config.ConnectSidecarMinPort; p <= a.config.ConnectSidecarMaxPort; p++ { _, used := usedPorts[p] if !used { - sidecar.Port = p + sidecarPort = p break } } } } // If no ports left (or auto ports disabled) fail - if sidecar.Port < 1 { + if sidecarPort < 1 { // If ports are set to zero explicitly, config builder switches them to // `-1`. In this case don't show the actual values since we don't know what // was actually in config (zero or negative) and it might be confusing, we // just know they explicitly disabled auto assignment. if a.config.ConnectSidecarMinPort < 1 || a.config.ConnectSidecarMaxPort < 1 { - return nil, nil, "", fmt.Errorf("no port provided for sidecar_service " + + return 0, fmt.Errorf("no port provided for sidecar_service " + "and auto-assignment disabled in config") } - return nil, nil, "", fmt.Errorf("no port provided for sidecar_service and none "+ + return 0, fmt.Errorf("no port provided for sidecar_service and none "+ "left in the configured range [%d, %d]", a.config.ConnectSidecarMinPort, a.config.ConnectSidecarMaxPort) } - // Setup checks - checks, err := ns.Connect.SidecarService.CheckTypes() - if err != nil { - return nil, nil, "", err - } - - // Setup default check if none given - if len(checks) < 1 { - checks = []*structs.CheckType{ - { - Name: "Connect Sidecar Listening", - // Default to localhost rather than agent/service public IP. The checks - // can always be overridden if a non-loopback IP is needed. - TCP: ipaddr.FormatAddressPort(sidecar.Proxy.LocalServiceAddress, sidecar.Port), - Interval: 10 * time.Second, - }, - { - Name: "Connect Sidecar Aliasing " + ns.ID, - AliasService: ns.ID, - }, - } - } - - return sidecar, checks, token, nil + return sidecarPort, nil +} + +func sidecarDefaultChecks(serviceID string, localServiceAddress string, port int) []*structs.CheckType { + // Setup default check if none given + return []*structs.CheckType{ + { + Name: "Connect Sidecar Listening", + // Default to localhost rather than agent/service public IP. The checks + // can always be overridden if a non-loopback IP is needed. + TCP: ipaddr.FormatAddressPort(localServiceAddress, port), + Interval: 10 * time.Second, + }, + { + Name: "Connect Sidecar Aliasing " + serviceID, + AliasService: serviceID, + }, + } } diff --git a/agent/sidecar_service_test.go b/agent/sidecar_service_test.go index 7ced9720a7..cffe054c21 100644 --- a/agent/sidecar_service_test.go +++ b/agent/sidecar_service_test.go @@ -2,6 +2,7 @@ package agent import ( "fmt" + "github.com/hashicorp/consul/acl" "testing" "time" @@ -16,16 +17,13 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { } tests := []struct { - name string - maxPort int - preRegister *structs.ServiceDefinition - sd *structs.ServiceDefinition - token string - autoPortsDisabled bool - wantNS *structs.NodeService - wantChecks []*structs.CheckType - wantToken string - wantErr string + name string + sd *structs.ServiceDefinition + token string + wantNS *structs.NodeService + wantChecks []*structs.CheckType + wantToken string + wantErr string }{ { name: "no sidecar", @@ -141,42 +139,6 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { }, wantToken: "custom-token", }, - { - name: "no auto ports available", - // register another sidecar consuming our 1 and only allocated auto port. - preRegister: &structs.ServiceDefinition{ - Kind: structs.ServiceKindConnectProxy, - Name: "api-proxy-sidecar", - Port: 2222, // Consume the one available auto-port - Proxy: &structs.ConnectProxyConfig{ - DestinationServiceName: "api", - }, - }, - sd: &structs.ServiceDefinition{ - ID: "web1", - Name: "web", - Port: 1111, - Connect: &structs.ServiceConnect{ - SidecarService: &structs.ServiceDefinition{}, - }, - }, - token: "foo", - wantErr: "none left in the configured range [2222, 2222]", - }, - { - name: "auto ports disabled", - autoPortsDisabled: true, - sd: &structs.ServiceDefinition{ - ID: "web1", - Name: "web", - Port: 1111, - Connect: &structs.ServiceConnect{ - SidecarService: &structs.ServiceDefinition{}, - }, - }, - token: "foo", - wantErr: "auto-assignment disabled in config", - }, { name: "inherit tags and meta", sd: &structs.ServiceDefinition{ @@ -252,6 +214,64 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { token: "foo", wantErr: "reserved for internal use", }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hcl := ` + ports { + sidecar_min_port = 2222 + sidecar_max_port = 2222 + } + ` + a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl}) + defer a.Shutdown() + + ns := tt.sd.NodeService() + err := ns.Validate() + require.NoError(t, err, "Invalid test case - NodeService must validate") + + gotNS, gotChecks, gotToken, err := a.sidecarServiceFromNodeService(ns, tt.token) + if tt.wantErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantNS, gotNS) + require.Equal(t, tt.wantChecks, gotChecks) + require.Equal(t, tt.wantToken, gotToken) + }) + } +} + +func TestAgent_SidecarPortFromServiceIDLocked(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + tests := []struct { + name string + autoPortsDisabled bool + enterpriseMeta acl.EnterpriseMeta + maxPort int + port int + preRegister *structs.ServiceDefinition + serviceID string + wantPort int + wantErr string + }{ + { + name: "port pre-specified", + serviceID: "web1", + wantPort: 2222, + }, + { + name: "use auto ports", + serviceID: "web1", + port: 1111, + wantPort: 1111, + }, { name: "re-registering same sidecar with no port should pick same one", // Allow multiple ports to be sure we get the right one @@ -269,42 +289,27 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { LocalServicePort: 1111, }, }, - // Register same again but with different service port - sd: &structs.ServiceDefinition{ - ID: "web1", - Name: "web", - Port: 1112, - Connect: &structs.ServiceConnect{ - SidecarService: &structs.ServiceDefinition{}, + // Register same again + serviceID: "web1-sidecar-proxy", + wantPort: 2222, // Should claim the same port as before + }, + { + name: "all auto ports already taken", + // register another sidecar consuming our 1 and only allocated auto port. + preRegister: &structs.ServiceDefinition{ + Kind: structs.ServiceKindConnectProxy, + Name: "api-proxy-sidecar", + Port: 2222, // Consume the one available auto-port + Proxy: &structs.ConnectProxyConfig{ + DestinationServiceName: "api", }, }, - token: "foo", - wantNS: &structs.NodeService{ - EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), - Kind: structs.ServiceKindConnectProxy, - ID: "web1-sidecar-proxy", - Service: "web-sidecar-proxy", - Port: 2222, // Should claim the same port as before - LocallyRegisteredAsSidecar: true, - Proxy: structs.ConnectProxyConfig{ - DestinationServiceName: "web", - DestinationServiceID: "web1", - LocalServiceAddress: "127.0.0.1", - LocalServicePort: 1112, - }, - }, - wantChecks: []*structs.CheckType{ - { - Name: "Connect Sidecar Listening", - TCP: "127.0.0.1:2222", - Interval: 10 * time.Second, - }, - { - Name: "Connect Sidecar Aliasing web1", - AliasService: "web1", - }, - }, - wantToken: "foo", + wantErr: "none left in the configured range [2222, 2222]", + }, + { + name: "auto ports disabled", + autoPortsDisabled: true, + wantErr: "auto-assignment disabled in config", }, } for _, tt := range tests { @@ -329,7 +334,6 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { } ` } - a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl}) defer a.Shutdown() @@ -338,11 +342,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { require.NoError(t, err) } - ns := tt.sd.NodeService() - err := ns.ValidateForAgent() - require.NoError(t, err, "Invalid test case - NodeService must validate") - - gotNS, gotChecks, gotToken, err := a.sidecarServiceFromNodeService(ns, tt.token) + gotPort, err := a.sidecarPortFromServiceIDLocked(tt.port, structs.ServiceID{ID: tt.serviceID, EnterpriseMeta: tt.enterpriseMeta}) if tt.wantErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.wantErr) @@ -350,9 +350,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { } require.NoError(t, err) - require.Equal(t, tt.wantNS, gotNS) - require.Equal(t, tt.wantChecks, gotChecks) - require.Equal(t, tt.wantToken, gotToken) + require.Equal(t, tt.wantPort, gotPort) }) } } From 6b932b50991dd6169fd3157fbeeaf183440d1651 Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Thu, 28 Jul 2022 10:02:12 -0700 Subject: [PATCH 068/339] more tweaks to gatewayclass docs --- .../configuration/gatewayclass.mdx | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gatewayclass.mdx b/website/content/docs/api-gateway/configuration/gatewayclass.mdx index f231fed999..7690c452c9 100644 --- a/website/content/docs/api-gateway/configuration/gatewayclass.mdx +++ b/website/content/docs/api-gateway/configuration/gatewayclass.mdx @@ -7,13 +7,11 @@ description: >- # GatewayClass -This topic provides full details about the `GatewayClass` resource +This topic provides describes how to configure the `GatewayClass` resource, which is a generic Kubernetes gateway object used as a template for creating `Gateway` resources. ## Introduction -The `GatewayClass` resource is used as a template for creating `Gateway` resources. -The specification includes the name of the controller (`controllerName`) and an API object containing controller-specific configuration resources within the cluster (`parametersRef`). -The value of the `controllerName` field must be set to `hashicorp.com/consul-api-gateway-controller`. +The `GatewayClass` specification includes the name of the controller (`controllerName`) and an API object containing controller-specific configuration resources within the cluster (`parametersRef`). The value of the `controllerName` field must be set to `hashicorp.com/consul-api-gateway-controller`. When gateways are created from a `GatewayClass`, they use the parameters specified in the `GatewayClass` at the time of instantiation. @@ -23,10 +21,10 @@ The `GatewayClass` resource is a generic Kubernetes gateway object. For configur The following outline shows how to format the configurations in the `GatewayClass` object. Click on a property name to view details about the configuration. * [`controllerName`](#controllername): string | required -* [`parametersRef`](#parametersref): object | optional - * [`group`]([#parametersref-group): string | required is `parametersRef` is set - * [`kind`](#parametersref-kind): string | required is `parametersRef` is set - * [`name`](#parametersref-name): string | required is `parametersRef` is set +* [`parametersRef`](#parametersref): object | required + * [`group`](#parametersref): string | required if `parametersRef` is set + * [`kind`](#parametersref): string | required if `parametersRef` is set + * [`name`](#parametersref): string | required if `parametersRef` is set * [`description`](#description): string | optional ## Specification @@ -34,39 +32,24 @@ The following outline shows how to format the configurations in the `GatewayClas This topic provides details about the configuration parameters. ### controllerName -Specifies the name of the controller that manages the gateways generated by this class. The value must alwasy be `'hashicorp.com/consul-api-gateway-controller'`. +Specifies the name of the controller that manages the gateways generated by this class. +The value must always be `hashicorp.com/consul-api-gateway-controller`. * Type: string * Required: required ### parametersRef -Defines an object that specifies additional configurations required by the gateway controller. -* Type: object -* Required: required +Defines an API object that references additional configurations required by the gateway controller. The following table describes the fields that you must include in the `parametersRef` coniguration. -### parametersRef.group -Specifies the Kubernetes group that the `parametersRef` is a member of. The value must always be `api-gateway.consul.hashicorp.com`. - -The `parametersRef.group` is always the same across all deployments of Consul API Gateway. - -* Type: string -* Required: required - -### parametersRef.kind -Specifies the Kubernetes kind of entity that the `parametersRef` is. The value must always be `GatewayClassConfig`. - -This `parametersRef.kind` is always the same across all deployments of Consul API Gateway. - -* Type: string -* Required: required - -### parametersRef.name -Specfies the name of the `GatewayClassConfig` object. -* Type: object -* Required: required +| Parameter | Description | Type | Required | +| --- | --- | --- | --- | +| `group` | Specifies the Kubernetes group that the `parametersRef` is a member of.
The value must always be `api-gateway.consul.hashicorp.com`.
The `parametersRef.group` is always the same across all deployments of Consul API Gateway. | String | Required | +| `kind` | Specifies the type of Kubernetes object that the `parametersRef` configuration defines.
The value must always be `GatewayClassConfig`.
This `parametersRef.kind` is always the same across all deployments of Consul API Gateway. | String | Required | +| `name` | Specfies a name for the `GatewayClassConfig` object. | String | Required | ### description -Specifies a human-readable description of the gateway class. We recommend including a description so that a record exists that describes the gateway class's purpose. +Specifies a human-readable description of the gateway class. We recommend using the description field to describe the gateway class's purpose. + * Type: string * Required: optional @@ -86,6 +69,7 @@ The following example creates a gateway class called `test-gateway-class`: group: api-gateway.consul.hashicorp.com kind: GatewayClassConfig name: test-gateway-class-config + description: The gateway class is for creating test gateways class configurations ``` From 692767d88d8af71f961306f25f70ef0fdfaedc6f Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Thu, 28 Jul 2022 11:04:21 -0700 Subject: [PATCH 069/339] tweaks to the configuration overview page --- website/content/docs/api-gateway/configuration/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/index.mdx b/website/content/docs/api-gateway/configuration/index.mdx index fd1ac2a9dd..59e95e18a7 100644 --- a/website/content/docs/api-gateway/configuration/index.mdx +++ b/website/content/docs/api-gateway/configuration/index.mdx @@ -14,10 +14,10 @@ This topic provides an overview of the configuration items that enable Consul AP - [GatewayClass](/docs/api-gateway/configuration/gatewayclass): Defines a class of gateway resources that you can use as a template for creating gateways. - [Routes](/docs/api-gateway/configuration/routes): Specifies the path from the gateway to the backend service(s)client to the listener. -A basic Gateway object can simply use the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayclassname) `consul-api-gateway`. For additional configruation options follow the following steps. +You can implement the default [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayclassname) (`consul-api-gateway`) to create a basic Gateway object. You can also complete the following steps to create custom Gateways suitable for your environment: -1. Define a [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) containing your desired configurations. -1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) with [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass#parametersref-name) set to the name of the newly defined [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) -1. Define a [Gateway](/docs/api-gateway/configuration/gateway) with [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayclassname) set to the name of the newly defined [GatewayClass](/docs/api-gateway/configuration/gatewayclass) +1. Define a [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig) that contains your custom configurations. +1. Define a [GatewayClass](/docs/api-gateway/configuration/gatewayclass) and configure the [`parametersRef.name`](/docs/api-gateway/configuration/gatewayclass#parametersref-name) to reference the name of your [GatewayClassConfig](/docs/api-gateway/configuration/gatewayclassconfig). +1. Define a [Gateway](/docs/api-gateway/configuration/gateway) and configure the [`gatewayClassName`](/docs/api-gateway/configuration/gateway#gatewayclassname) to reference the name of your [GatewayClass](/docs/api-gateway/configuration/gatewayclass). From 7e2cece37634b94e0df926ad96a4625545b8f3fb Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Thu, 28 Jul 2022 11:38:50 -0700 Subject: [PATCH 070/339] tweaks to the basic usage topic --- website/content/docs/api-gateway/usage/basic-usage.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/usage/basic-usage.mdx b/website/content/docs/api-gateway/usage/basic-usage.mdx index d5cd194776..d06cdb2bd6 100644 --- a/website/content/docs/api-gateway/usage/basic-usage.mdx +++ b/website/content/docs/api-gateway/usage/basic-usage.mdx @@ -8,6 +8,8 @@ description: >- # Basic Usage +This topic describes the basic workflow for implementing Consul API Gateway configurations. + 1. Verify that the [requirements](/docs/api-gateway/consul-api-gateway-install#requirments) have been met. 1. Verify that the Consul API Gateway CRDs and controller have been installed and applied (see [Installation](/docs/api-gateway/consul-api-gateway-install)). 1. Configure the artifacts as describe in the [Configuration](/docs/api-gateway/configuration) section. @@ -79,7 +81,11 @@ consul-api-gateway 0.1.0 ## Error Messages -If the error message is not listed on this page, it may be listed on the main [Consul Common errors][consul-common-errors] page. If the error message is not listed on that page either, please consider following our general [Troubleshooting Guide][troubleshooting] or reach out to us in [Discuss](https://discuss.hashicorp.com/). +This topic provides information about potential error messages associated with Consul API Gateway. If your receive an error message that does not appear in this section, refer to the following resources: + +* [Common Consul errors](/docs/troubleshoot/common-errors#common-errors-on-kubernetes) +* [Consul troubleshooting guide](/docs/troubleshoot/common-errors) +* [Consul Discuss forum](https://discuss.hashicorp.com/) **Note**: For guidance on how to install `consul-k8s`, visit the +-> **Tip:** For guidance on how to install `consul-k8s`, visit the [Installing the Consul K8s CLI](/docs/k8s/installation/install-cli) documentation. This topic describes the commands and available options for using `consul-k8s`. From e32f16cb45f264eed729cf089fd5ac18dcc74f3f Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 16:58:43 -0400 Subject: [PATCH 189/339] See -> Refer to the Co-authored-by: Tu Nguyen --- website/content/docs/k8s/k8s-cli.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 90a835aa04..f1e66bc91e 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -112,7 +112,7 @@ $ consul-k8s proxy list | `-all-namespaces`, `-A` | `Boolean` List pods in all Kubernetes namespaces. | `false` | | `-namespace`, `-n` | `String` The Kubernetes namespace to list proxies in. | Current [kubeconfig](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) namespace. | -See [Global Options](#global-options) for additional options that you can use +Refer to the [Global Options](#global-options) for additional options that you can use when installing Consul on Kubernetes. This command will list proxies alongside their `Type`. Types of proxies include From 46c2927eadd63299bff01eb91061540d59b0ed35 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 17:03:01 -0400 Subject: [PATCH 190/339] Remove extra space from commands --- website/content/docs/k8s/k8s-cli.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index f1e66bc91e..cfd70abf8f 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -75,22 +75,22 @@ $ consul-k8s install -preset=secure -namespace=myNS The following example commands install Consul on Kubernetes using custom values, files, or strings that are set via flags. The underlying Consul-on-Kubernetes Helm chart uses the flags to customize the installation. The flags are comparable to the `helm install` [flags](https://helm.sh/docs/helm/helm_install/#helm-install). ```shell-session - $ consul-k8s install -set key=value +$ consul-k8s install -set key=value ``` ```shell-session - $ consul-k8s install -set key1=value1 -set key2=value2 +$ consul-k8s install -set key1=value1 -set key2=value2 ``` ```shell-session - $ consul-k8s install -set-file config1=value1.conf +$ consul-k8s install -set-file config1=value1.conf ``` ```shell-session - $ consul-k8s install -set-file config1=value1.conf -set-file config2=value2.conf +$ consul-k8s install -set-file config1=value1.conf -set-file config2=value2.conf ``` ```shell-session - $ consul-k8s install -set-string key=value-bool +$ consul-k8s install -set-string key=value-bool ``` ### `proxy` From 2946dba663aae6ab3cae16f42b1a15a995726fb1 Mon Sep 17 00:00:00 2001 From: Eddie Rowe <74205376+eddie-rowe@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:05:39 -0500 Subject: [PATCH 191/339] Add Service Discovery Use Case --- .../usecases/what-is-service-discovery.mdx | 96 ++++++++++++++++++ .../img/what_is_service_discovery_1.png | Bin 0 -> 125019 bytes .../img/what_is_service_discovery_2.png | Bin 0 -> 126313 bytes .../img/what_is_service_discovery_3.png | Bin 0 -> 137353 bytes .../img/what_is_service_discovery_4.png | Bin 0 -> 136071 bytes .../img/what_is_service_discovery_5.png | Bin 0 -> 133985 bytes 6 files changed, 96 insertions(+) create mode 100644 website/content/docs/intro/usecases/what-is-service-discovery.mdx create mode 100644 website/public/img/what_is_service_discovery_1.png create mode 100644 website/public/img/what_is_service_discovery_2.png create mode 100644 website/public/img/what_is_service_discovery_3.png create mode 100644 website/public/img/what_is_service_discovery_4.png create mode 100644 website/public/img/what_is_service_discovery_5.png diff --git a/website/content/docs/intro/usecases/what-is-service-discovery.mdx b/website/content/docs/intro/usecases/what-is-service-discovery.mdx new file mode 100644 index 0000000000..655ff99faa --- /dev/null +++ b/website/content/docs/intro/usecases/what-is-service-discovery.mdx @@ -0,0 +1,96 @@ +--- +layout: docs +page_title: What is a service mesh? +description: >- + Learn what service discovery is, its benefits, and how it works. + Service mesh can solve many of the modern challenges that exist in multi-platform and multi-cloud application architectures, ranging from security to application resiliency. +--- + +# What is Service Discovery? + +_Service discovery_ helps you discover, track, and monitor the health of services within a network. Service discovery registers and maintains a record of all your services in a _service catalog_. This service catalog acts as a single source of truth that allows your services to query and communicate with each other. + +## Benefits of Service Discovery + +Service discovery provides benefits for all organizations, ranging from simplified scalability to improved application resiliency. Some of the benefits of service discovery include: + +- Dynamic IP address and port discovery +- Simplified horizontal service scaling +- Abstracts discovery logic away from applications +- Reliable service communication ensured by health checks +- Load balances requests across healthy service instances +- Faster deployment times achieved by high-speed discovery +- Automated service registration and de-registration + +## How does a service mesh work? + +Service discovery uses a service's identity instead of traditional access information (IP address and port). This allows you to dynamically map services and track any changes within a service catalog. Service consumers (users or other services) then use DNS to dynamically retrieve other service’s access information from the service catalog. The lifecycle of a service may look like the following: + +A service consumer communicates with the “Web” service via a unique Consul DNS entry provided by the service catalog. + +![Example diagram of how service consumers query for services](/img/what_is_service_discovery_1.png) + +A new instance of the “Web” service registers itself to the service catalog with its IP address and port. As new instances of your services are registered to the service catalog, they will participate in the load balancing pool for handling service consumer requests. + +![Example diagram of how a service is registered to the service catalog](/img/what_is_service_discovery_2.png) + +The service catalog is dynamically updated as new instances of the service are added and legacy or unhealthy service instances are removed. Removed services will no longer participate in the load balancing pool for handling service consumer requests. + +![Example diagram of how unhealthy services are removed from the service catalog](/img/what_is_service_discovery_3.png) + +## What is Service Discovery in Microservices? + +In a microservices application, the set of active service instances changes frequently across a large, dynamic environment. These service instances rely on a service catalog to retrieve the most up-to-date access information from the respective services. A reliable service catalog is especially important for service discovery in microservices to ensure healthy, scalable, and highly responsive application operation. + +## What are the two Main Types of Service Discovery? + +There are two main service‑discovery patterns: _client-side_ discovery and _server-side_ discovery. + +In systems that use client‑side discovery, the service consumer is responsible for determining the access information of available service instances and load balancing requests between them. + +1. The service consumer queries the service catalog +2. The service catalog retrieves and returns all access information +3. The service consumer selects a healthy downstream service and makes requests directly to it + +![Example diagram of client-side discovery concept](/img/what_is_service_discovery_4.png) + +In systems that use server‑side discovery, the service consumer uses an intermediary to query the service catalog and make requests to them. + +1. The service consumer queries an intermediary (Consul) +2. The intermediary queries the service catalog and routes requests to the available service instances. + +![Example diagram of server-side discovery concept](/img/what_is_service_discovery_5.png) + +For modern applications, this discovery method is advantageous because developers can make their applications faster and more lightweight by decoupling and centralizing service discovery logic. + +## Service Discovery vs Load Balancing + +Service discovery and load balancing share a similarity in distributing requests to back end services, but differ in many important ways. + +Traditional load balancers are not designed for rapid registration and de-registration of services, nor are they designed for high-availability. By contrast, service discovery systems use multiple nodes that maintain the service registry state and a peer-to-peer state management system for increased resilience across any type of infrastructure. + +For modern, cloud-based applications, service discovery is the preferred method for directing traffic to the right service provider due to its ability to scale and remain resilient, independent of infrastructure. + +## How do you implement Service Discovery? + +You can implement service discovery systems across any type of infrastructure, whether it is on-premise or in the cloud. Service discovery is a native feature of many container orchestrators such as Kubernetes or Nomad. There are also platform-agnostic service discovery methods available for non-container workloads such as VMs and serverless technologies. Implementing a resilient service discovery system involves creating a set of servers that maintain and facilitate service registry operations. You can achieve this by installing a service discovery system or using a managed service discovery service. + +## What is Consul? + +Consul is a service networking solution that lets you automate network configurations, discover services, and enable secure connectivity across any cloud or runtime. With these features, Consul helps you solve the complex networking and security challenges of operating microservices and cloud infrastructure (multi-cloud and hybrid cloud). You can use these features independently or together to achieve [zero trust](https://www.hashicorp.com/solutions/zero-trust-security) security. + +Consul’s service discovery capabilities help you discover, track, and monitor the health of services within a network. Consul acts as a single source of truth that allows your services to query and communicate with each other. + +You can use Consul with virtual machines (VMs), containers, serverless technologies, or with container orchestration platforms, such as [Nomad](https://www.nomadproject.io/) and Kubernetes. Consul is platform agnostic which makes it a great fit for all environments, including legacy platforms. + +Consul is available as a [self-managed](/downloads) project or as a fully managed service mesh solution ([HCP Consul](https://portal.cloud.hashicorp.com/sign-in?utm_source=consul_docs)). HCP Consul enables users to discover and securely connect services without the added operational burden of maintaining a service mesh on their own. + +## Next + +Get started with service discovery today by leveraging Consul on HCP, Consul on Kubernetes, or Consul on VMs. Prepare your organization for the future of multi-cloud and embrace a [zero-trust](https://www.hashicorp.com/solutions/zero-trust-security) architecture. + +Feel free to get started with Consul by exploring one of these Consul Learn tutorials: + +[Getting Started with Consul on VMs](https://learn.hashicorp.com/collections/consul/getting-started) +[Getting Started with Consul on HCP](https://learn.hashicorp.com/collections/consul/cloud-get-started) +[Getting Started with Consul on Kubernetes](https://learn.hashicorp.com/collections/consul/gs-consul-service-mesh) \ No newline at end of file diff --git a/website/public/img/what_is_service_discovery_1.png b/website/public/img/what_is_service_discovery_1.png new file mode 100644 index 0000000000000000000000000000000000000000..9b2ecc0b36a7bca37b9638a607ff45470c443d9d GIT binary patch literal 125019 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 MU(qVPaP$8E0Y;bvLI3~& literal 0 HcmV?d00001 diff --git a/website/public/img/what_is_service_discovery_2.png b/website/public/img/what_is_service_discovery_2.png new file mode 100644 index 0000000000000000000000000000000000000000..19c10e135bc105ee3867bad869822715b098a58c GIT binary patch literal 126313 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�|IIb|-*g@C#s$z$_2)!t~`(&$TP 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�)a^SRa=UI7GsnvM27G1V)7#^4OHi7pA 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 Oqlb(SX8-PR_5T4LYCit} literal 0 HcmV?d00001 diff --git a/website/public/img/what_is_service_discovery_5.png b/website/public/img/what_is_service_discovery_5.png new file mode 100644 index 0000000000000000000000000000000000000000..b7704e8786b05f468486c9a6fc56cbf45bdc8c88 GIT binary patch literal 133985 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 literal 0 HcmV?d00001 From f1f5776575f867441c15c13817bb3778a762be3e Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 17:08:18 -0400 Subject: [PATCH 192/339] Add colon Co-authored-by: Tu Nguyen --- website/content/docs/k8s/k8s-cli.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index cfd70abf8f..7da9ba3640 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -115,7 +115,7 @@ $ consul-k8s proxy list Refer to the [Global Options](#global-options) for additional options that you can use when installing Consul on Kubernetes. -This command will list proxies alongside their `Type`. Types of proxies include +This command will list proxies alongside their `Type`. Types of proxies include: - `Sidecar`: these will be the majority of pods in the cluster. They run the proxy in a sidecar pattern to network the pod as a service in the mesh. From b641be8466bb055a340b616b30203770409ee6d0 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 17:09:33 -0400 Subject: [PATCH 193/339] Clean up external links about proxies Co-authored-by: Tu Nguyen --- website/content/docs/k8s/k8s-cli.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 7da9ba3640..1923833701 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -120,13 +120,13 @@ This command will list proxies alongside their `Type`. Types of proxies include: - `Sidecar`: these will be the majority of pods in the cluster. They run the proxy in a sidecar pattern to network the pod as a service in the mesh. - `API Gateway`: these pods run a proxy to manage connections with networks - outside of the Consul cluster. [Read more about API gateways](/docs/api-gateway). + outside of the Consul cluster. Read more about [API gateways](/docs/api-gateway). - `Ingress Gateway`: these pods run a proxy to manage ingress into the - Kubernetes cluster. [Read more about ingress gateways](/docs/k8s/connect/ingress-gateways). + Kubernetes cluster. Read more about [ingress gateways](/docs/k8s/connect/ingress-gateways). - `Terminating Gateway`: these pods run a proxy to control connections to - external services. [Read more about terminating gateways](/docs/k8s/connect/terminating-gateways). + external services. Read more about [terminating gateways](/docs/k8s/connect/terminating-gateways). - `Mesh Gateway`: these pods run a proxy to manage connections between - Consul clusters connected using mesh federation. [Read more about Consul Mesh Federation](/docs/k8s/installation/multi-cluster/kubernetes). + Consul clusters connected using mesh federation. Read more about [Consul Mesh Federation](/docs/k8s/installation/multi-cluster/kubernetes). #### Example Commands From bcad9e8fcec4a23cd0b1cbc7a650278483256c69 Mon Sep 17 00:00:00 2001 From: Eddie Rowe <74205376+eddie-rowe@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:15:02 -0500 Subject: [PATCH 194/339] add doc to content nav --- website/data/docs-nav-data.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 4bb776ddee..3ef7619d87 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -14,6 +14,10 @@ "path": "intro/usecases", "hidden": true }, + { + "title": "What is Service Discovery?", + "path": "intro/usecases/what-is-service-discovery" + }, { "title": "What is a Service Mesh?", "path": "intro/usecases/what-is-a-service-mesh" From 07b240e79c9e1536786902808d57a799311e9259 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 17:25:25 -0400 Subject: [PATCH 195/339] Combine shell sessions --- website/content/docs/k8s/k8s-cli.mdx | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 1923833701..2f470a9b3b 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -167,9 +167,6 @@ Display all pods across all namespaces which run proxies managed by Consul. ```shell-session $ consul-k8s proxy list -A -``` - -``` Namespace: All namespaces Namespace Name Type @@ -217,9 +214,6 @@ Get the configuration summary for the Envoy proxy running on the Pod ```shell-session $ consul-k8s proxy read backend-658b679b45-d5xlb -``` - -``` Envoy configuration for backend-658b679b45-d5xlb in namespace default: ==> Clusters (5) @@ -262,9 +256,6 @@ domain name which includes `"default"`. Display only clusters and listeners. ```shell-session $ consul-k8s proxy read backend-658b679b45-d5xlb -fqdn default -clusters -listeners -``` - -``` ==> Filters applied Fully qualified domain names containing: default @@ -291,9 +282,6 @@ same as what is displayed in the table output above, but in a JSON format. ```shell-session $ consul-k8s proxy read backend-658b679b45-d5xlb -o json -``` - -``` { "backend-658b679b45-d5xlb": { "clusters": [ @@ -442,15 +430,11 @@ be output for each service as a JSON map. The the configuration for the service you want to inspect. See the [Envoy config dump documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/config_dump.proto) -for more information on the structure of the config dump. +for more information on the structure of the config dump. The output is +truncated here for brevity. It follows the format below: ```shell-session $ consul-k8s proxy read backend-658b679b45-d5xlb -o raw -``` - -The output is truncated here for brevity. It follows the format below: - -``` { "backend-658b679b45-d5xlb": { "clusters": { From 71027a4ce11cedd11fe930a4451d3b859f42d45c Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 17:28:36 -0400 Subject: [PATCH 196/339] Clean up raw copy Co-authored-by: Tu Nguyen --- website/content/docs/k8s/k8s-cli.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 2f470a9b3b..c013ed74d8 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -424,9 +424,9 @@ $ consul-k8s proxy read backend-658b679b45-d5xlb -o json ``` Get the raw Envoy configuration dump and clusters information for the Envoy -proxy running on the Pod `backend-658b679b45-d5xlb`. The raw configuration will -be output for each service as a JSON map. The -[JQ command line tool](https://stedolan.github.io/jq/) can be used to index into +proxy running on the Pod `backend-658b679b45-d5xlb`. This command will return +the raw configuration for each service as a JSON Map. You can use the +[JQ command line tool](https://stedolan.github.io/jq/) to index into the configuration for the service you want to inspect. See the [Envoy config dump documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/config_dump.proto) From 31afaa80970a4231c20a110a187810ce506e6c07 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Mon, 8 Aug 2022 17:15:42 -0400 Subject: [PATCH 197/339] Track last user of a port --- sdk/freeport/freeport.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sdk/freeport/freeport.go b/sdk/freeport/freeport.go index 40d9cdb3b8..6eda1d4279 100644 --- a/sdk/freeport/freeport.go +++ b/sdk/freeport/freeport.go @@ -83,6 +83,10 @@ var ( // stopWg is used to keep track of background goroutines that are still // alive. Only really exists for the safety of reset() during unit tests. stopWg sync.WaitGroup + + // portLastUser associates ports with a test name in order to debug + // which test may be leaking unclosed TCP connections. + portLastUser map[int]string ) // initialize is used to initialize freeport. @@ -127,6 +131,8 @@ func initialize() { stopWg.Add(1) stopCh = make(chan struct{}) + + portLastUser = make(map[int]string) // Note: we pass this param explicitly to the goroutine so that we can // freely recreate the underlying stop channel during reset() after closing // the original. @@ -166,6 +172,7 @@ func reset() { freePorts = nil pendingPorts = nil + portLastUser = nil total = 0 } @@ -196,7 +203,7 @@ func checkFreedPortsOnce() { freePorts.PushBack(port) remove = append(remove, elem) } else { - logf("WARN", "port %d still being used", port) + logf("WARN", "port %d still being used by %q", port, portLastUser[port]) } } @@ -416,6 +423,11 @@ func GetN(t TestingT, n int) []int { t.Fatalf("failed to take %v ports: %w", n, err) } logf("DEBUG", "Test %q took ports %v", t.Name(), ports) + mu.Lock() + for _, p := range ports { + portLastUser[p] = t.Name() + } + mu.Unlock() t.Cleanup(func() { Return(ports) logf("DEBUG", "Test %q returned ports %v", t.Name(), ports) From f0c916479b3b71d8f390957d8d30773103e2e41f Mon Sep 17 00:00:00 2001 From: boruszak Date: Mon, 8 Aug 2022 16:32:38 -0500 Subject: [PATCH 198/339] UI instructions --- .../cluster-peering/create-manage-peering.mdx | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index 1240603b91..c1acacc7eb 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -22,16 +22,19 @@ Then, complete the following steps in order: 1. Export services between clusters 1. Authorize services for peers +You can generate peering tokens and initiate connections on any available agent using either the API or the Consul UI. If you use the API, we recommend performing these operations through a client agent in the partition you want to connect. + +The UI does not currently support exporting services between clusters or authorizing services for peers. + ### Create a peering token To begin the cluster peering process, generate a peering token in one of your clusters. The other cluster uses this token to establish the peering connection. Everytime you generate a peering token, a single-use establishment secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. -You can generate peering tokens and initiate connections on any available agent using either the Consul UI or the API. If you use the API, we recommend performing these operations through a client agent in the partition you want to connect. - + In `cluster-01`, issue a request for a peering token. ```shell-session @@ -56,16 +59,11 @@ Create a JSON file that contains the first cluster's name and the peering token. -1. In the Consul UI associated with `cluster-01`, click **Peers**. +1. In the Consul UI for the datacenter associated with `cluster-01`, click **Peers**. 1. Click **Add peer connection**. -1. In the **Name of peer** field, enter `cluster-02`. Then, click **Generate token**. -1. Copy the token. Be careful not to lose the token, as you cannot view the token again after leaving this screen. -1. Switch to the UI associated with `cluster 02`. Then, click **Peers** and then **Add peer connection**. -1. Click **Establish peering**. -1. In the **Name of peer** field, enter `cluster-01`. Then paste the token in the **Token** field. -1. Click **Add peer**. - -The +1. In the **Generate token** tab, enter `cluster-02` in the **Name of peer** field. +1. Click the **Generate token** button. +1. Copy the token before you proceed. Be careful not to lose the token, as you cannot view the token again after leaving this screen. If you lose your token, you must generate a new one. @@ -75,6 +73,7 @@ Next, use the peering token to establish a secure connection between the cluster + In one of the client agents in "cluster-02," use `peering_token.json` to establish the peering connection. This endpoint does not generate an output unless there is an error. ```shell-session @@ -86,6 +85,10 @@ When you connect server agents through cluster peering, they peer their default +1. In the Consul UI for the datacenter associated with `cluster 02`, click **Peers** and then **Add peer connection**. +1. Click **Establish peering**. +1. In the **Name of peer** field, enter `cluster-01`. Then paste the peering token in the **Token** field. +1. Click **Add peer**. @@ -93,8 +96,6 @@ When you connect server agents through cluster peering, they peer their default After you establish a connection between the clusters, you need to create a configuration entry that defines the services that are available for other clusters. Consul uses this configuration entry to advertise service information and support service mesh connections across clusters. - - First, create a configuration entry and specify the `Kind` as `"exported-services"`. @@ -127,19 +128,11 @@ $ consul config write peering-config.hcl ``` Before you proceed, wait for the clusters to sync and make services available to their peers. You can issue an endpoint query to [check the peered cluster status](#check-peered-cluster-status). - - - - - - ### Authorize services for peers Before you can call services from peered clusters, you must set service intentions that authorize those clusters to use specific services. Consul prevents services from being exported to unauthorized clusters. - - First, create a configuration entry and specify the `Kind` as `"service-intentions"`. Declare the service on "cluster-02" that can access the service in "cluster-01." The following example sets service intentions so that "frontend-service" can access "backend-service." @@ -166,12 +159,6 @@ Then, add the configuration entry to your cluster. ```shell-session $ consul config write peering-intentions.hcl ``` - - - - - - ## Manage peering connections @@ -220,6 +207,9 @@ $ curl http://127.0.0.1:8500/v1/peerings +In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in a datacenter. + +The name that appears in the list is the name of the cluster in a different datacenter with an established peering connection. @@ -252,6 +242,7 @@ $ curl http://127.0.0.1:8500/v1/peering/cluster-02 +In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in a datacenter. Click the name of a peered cluster to view additional details about the peering connection. @@ -259,9 +250,7 @@ $ curl http://127.0.0.1:8500/v1/peering/cluster-02 You can check the status of your peering connection to perform health checks. - - -To confirm that the peering connection between your clusters remains healthy, [query the `/health/service` endpoint](/api-docs/health) of one cluster from the other cluster. For example, in "cluster-02," query the endpoint and add the `peer=cluster-01` query parameter to the end of the URL. +To confirm that the peering connection between your clusters remains healthy, query the [`health/service` endpoint](/api-docs/health) of one cluster from the other cluster. For example, in "cluster-02," query the endpoint and add the `peer=cluster-01` query parameter to the end of the URL. ```shell-session $ curl \ @@ -269,12 +258,6 @@ $ curl \ ``` A successful query includes service information in the output. - - - - - - ### Delete peering connections @@ -282,7 +265,8 @@ You can disconnect the peered clusters by deleting their connection. Deleting a -In "cluster-01," request the deletion through the [`/peering/` endpoint](api-docs/peering#delete-a-peering-connection). + +In "cluster-01," request the deletion through the [`/peering/ endpoint`](/api-docs/peering#delete-a-peering-connection). ```shell-session $ curl --request DELETE http://127.0.0.1:8500/v1/peering/cluster-02 From 0dbb3a634fceff7dce2a6bae7ce137d44e1125fa Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 17:32:47 -0400 Subject: [PATCH 199/339] Clean up copy for the raw example --- website/content/docs/k8s/k8s-cli.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index c013ed74d8..4f6d86b324 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -424,14 +424,15 @@ $ consul-k8s proxy read backend-658b679b45-d5xlb -o json ``` Get the raw Envoy configuration dump and clusters information for the Envoy -proxy running on the Pod `backend-658b679b45-d5xlb`. This command will return -the raw configuration for each service as a JSON Map. You can use the +proxy running on the Pod `backend-658b679b45-d5xlb`. This command will return +the raw configuration for each service as JSON. You can use the [JQ command line tool](https://stedolan.github.io/jq/) to index into the configuration for the service you want to inspect. -See the [Envoy config dump documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/config_dump.proto) -for more information on the structure of the config dump. The output is -truncated here for brevity. It follows the format below: +Refer to the [Envoy config dump documentation](https://www.envoyproxy.io/docs/envoy/latest/api-v3/admin/v3/config_dump.proto) +for more information on the structure of the config dump. + +The following output is truncated for brevity. ```shell-session $ consul-k8s proxy read backend-658b679b45-d5xlb -o raw From c3326d319ad0bcd523f09db0ee1e814b233c08a4 Mon Sep 17 00:00:00 2001 From: DanStough Date: Tue, 2 Aug 2022 14:57:58 -0400 Subject: [PATCH 200/339] docs: destination docs for k8s --- .../docs/k8s/connect/terminating-gateways.mdx | 71 +++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index e8c5ec845f..13da908b4f 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -88,6 +88,58 @@ Registering the external services with Consul is a multi-step process: ### Register external services with Consul +There are two ways to register an external service with Consul: +1. If [`TransparentProxy`](/docs/k8s/helm#v-connectinject-transparentproxy) is enabled, you can declare external endpoints in the [`Destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of `service-defaults`. +1. You can add the service as a node in the Consul catalog. + +#### Register an external service as a Destination + +`Destination` fields allow clients to dial the external service directly and are valid only in [`TransparentProxy`](/docs/k8s/helm#v-connectinject-transparentproxy) mode. +The following table describes traffic behaviors when using `Destination`s to route traffic through a terminating gateway: + +| External Services Layer | Client dials | Client uses TLS | Allowed | Notes | +|---|---|---|---|---| +| L4 | Hostname | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | +| L4 | IP | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | +| L4 | Hostname | No | Not allowed | The sidecar is not protocol aware and can not identify traffic going to the external service. | +| L4 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | +| L7 | Hostname | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | +| L7 | IP | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | +| L7 | Hostname | No | Allowed | A `Host` or `:authority` header is required. | +| L7 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | + +You can provide a `caFile` to secure traffic between unencrypted clients that connect to external services through the terminating gateway. +Refer to [Create the configuration entry for the terminating gateway](/docs/k8s/connect/terminating-gateways#create-the-configuration-entry-for-the-terminating-gateway) for details. + +Create a `service-defaults` custom resource for the external service: + + + +```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: ServiceDefaults + metadata: + name: example-https + spec: + protocol: tcp + destination: + addresses: + - "example.com" + port: 443 +``` + + + +Apply the `ServiceDefaults` resource with `kubectl apply`: + +```shell-session +$ kubectl apply --filename service-defaults.yaml +``` + +All other terminating gateway operations can use the name of the `service-defaults` in place of a typical Consul service name. + +#### Register an external service as a Catalog Node + -> **Note:** Normal Consul services are registered with the Consul client on the node that they're running on. Since this is an external service, there is no Consul node to register it onto. Instead, we will make up a node name and register the @@ -205,15 +257,14 @@ metadata: spec: services: - name: example-https - caFile: /etc/ssl/certs/ca-certificates.crt ``` -If TLS is enabled, you must include the `caFile` parameter that points to the system trust store of the terminating gateway container. By default, the trust store is located in the `/etc/ssl/certs/ca-certificates.crt` directory. - +-> **NOTE**: If TLS is enabled for external services registered through the Consul catalog, you must include the `caFile` parameter that points to the system trust store of the terminating gateway container. +By default, the trust store is located in the `/etc/ssl/certs/ca-certificates.crt` directory. Configure the `caFile` parameter to point to the `/etc/ssl/cert.pem` directory if TLS is enabled and you are using one of the following components: - * Consul Helm chart 0.43 or older + * Consul Helm chart 0.43 or older * Or an Envoy image with an alpine base image Apply the `TerminatingGateway` resource with `kubectl apply`: @@ -313,6 +364,18 @@ deployment "static-client" successfully rolled out You can verify connectivity of the static-client and terminating gateway via a curl command: + + ```shell-session $ kubectl exec deploy/static-client -- curl -vvvs --header "Host: example-https.com" http://localhost:1234/ ``` + + + + + +```shell-session +$ kubectl exec deploy/static-client -- curl -vvvs https://example.com/ +``` + + From cfd3e87ddeea2b85879c9707a406f70198caf391 Mon Sep 17 00:00:00 2001 From: Eddie Rowe <74205376+eddie-rowe@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:00:28 -0500 Subject: [PATCH 201/339] Apply suggestions from code review Co-authored-by: Tu Nguyen --- .../content/docs/intro/usecases/what-is-service-discovery.mdx | 4 ++-- website/data/docs-nav-data.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/content/docs/intro/usecases/what-is-service-discovery.mdx b/website/content/docs/intro/usecases/what-is-service-discovery.mdx index 655ff99faa..b894853b75 100644 --- a/website/content/docs/intro/usecases/what-is-service-discovery.mdx +++ b/website/content/docs/intro/usecases/what-is-service-discovery.mdx @@ -42,7 +42,7 @@ The service catalog is dynamically updated as new instances of the service are a In a microservices application, the set of active service instances changes frequently across a large, dynamic environment. These service instances rely on a service catalog to retrieve the most up-to-date access information from the respective services. A reliable service catalog is especially important for service discovery in microservices to ensure healthy, scalable, and highly responsive application operation. -## What are the two Main Types of Service Discovery? +## What are the two main types of service discovery? There are two main service‑discovery patterns: _client-side_ discovery and _server-side_ discovery. @@ -85,7 +85,7 @@ You can use Consul with virtual machines (VMs), containers, serverless technolog Consul is available as a [self-managed](/downloads) project or as a fully managed service mesh solution ([HCP Consul](https://portal.cloud.hashicorp.com/sign-in?utm_source=consul_docs)). HCP Consul enables users to discover and securely connect services without the added operational burden of maintaining a service mesh on their own. -## Next +## Next steps Get started with service discovery today by leveraging Consul on HCP, Consul on Kubernetes, or Consul on VMs. Prepare your organization for the future of multi-cloud and embrace a [zero-trust](https://www.hashicorp.com/solutions/zero-trust-security) architecture. diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 3ef7619d87..126cf11b8d 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -15,7 +15,7 @@ "hidden": true }, { - "title": "What is Service Discovery?", + "title": "Service Discovery", "path": "intro/usecases/what-is-service-discovery" }, { From 419c5ea3658d153063a2d8eea1d334a7d6ddab38 Mon Sep 17 00:00:00 2001 From: Eddie Rowe <74205376+eddie-rowe@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:09:47 -0500 Subject: [PATCH 202/339] add suggestions from code review --- .../docs/intro/usecases/what-is-a-service-mesh.mdx | 4 ++-- .../intro/usecases/what-is-service-discovery.mdx | 12 ++++++------ website/data/docs-nav-data.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/website/content/docs/intro/usecases/what-is-a-service-mesh.mdx b/website/content/docs/intro/usecases/what-is-a-service-mesh.mdx index ecae8a8274..17fd93e269 100644 --- a/website/content/docs/intro/usecases/what-is-a-service-mesh.mdx +++ b/website/content/docs/intro/usecases/what-is-a-service-mesh.mdx @@ -1,12 +1,12 @@ --- layout: docs -page_title: What is a service mesh? +page_title: Service Mesh description: >- Learn what a service mesh is, its benefits, and how it works. A service mesh can solve many of the modern challenges that exist in multi-platform and multi-cloud application architectures, ranging from security to application resiliency. --- -# What is a Service Mesh? +# What is a service mesh? A _service mesh_ is a dedicated network layer that provides secure service-to-service communication within and across infrastructure, including on-premises and cloud environments. Service meshes are often used with a microservice architectural pattern, but can provide value in any scenario where complex networking is involved. diff --git a/website/content/docs/intro/usecases/what-is-service-discovery.mdx b/website/content/docs/intro/usecases/what-is-service-discovery.mdx index b894853b75..820e5b7681 100644 --- a/website/content/docs/intro/usecases/what-is-service-discovery.mdx +++ b/website/content/docs/intro/usecases/what-is-service-discovery.mdx @@ -1,16 +1,16 @@ --- layout: docs -page_title: What is a service mesh? +page_title: Service Discovery description: >- Learn what service discovery is, its benefits, and how it works. Service mesh can solve many of the modern challenges that exist in multi-platform and multi-cloud application architectures, ranging from security to application resiliency. --- -# What is Service Discovery? +# What is service discovery? _Service discovery_ helps you discover, track, and monitor the health of services within a network. Service discovery registers and maintains a record of all your services in a _service catalog_. This service catalog acts as a single source of truth that allows your services to query and communicate with each other. -## Benefits of Service Discovery +## Benefits of service discovery Service discovery provides benefits for all organizations, ranging from simplified scalability to improved application resiliency. Some of the benefits of service discovery include: @@ -38,7 +38,7 @@ The service catalog is dynamically updated as new instances of the service are a ![Example diagram of how unhealthy services are removed from the service catalog](/img/what_is_service_discovery_3.png) -## What is Service Discovery in Microservices? +## What is service discovery in microservices? In a microservices application, the set of active service instances changes frequently across a large, dynamic environment. These service instances rely on a service catalog to retrieve the most up-to-date access information from the respective services. A reliable service catalog is especially important for service discovery in microservices to ensure healthy, scalable, and highly responsive application operation. @@ -63,7 +63,7 @@ In systems that use server‑side discovery, the service consumer uses an interm For modern applications, this discovery method is advantageous because developers can make their applications faster and more lightweight by decoupling and centralizing service discovery logic. -## Service Discovery vs Load Balancing +## Service discovery vs load balancing Service discovery and load balancing share a similarity in distributing requests to back end services, but differ in many important ways. @@ -71,7 +71,7 @@ Traditional load balancers are not designed for rapid registration and de-regist For modern, cloud-based applications, service discovery is the preferred method for directing traffic to the right service provider due to its ability to scale and remain resilient, independent of infrastructure. -## How do you implement Service Discovery? +## How do you implement service discovery? You can implement service discovery systems across any type of infrastructure, whether it is on-premise or in the cloud. Service discovery is a native feature of many container orchestrators such as Kubernetes or Nomad. There are also platform-agnostic service discovery methods available for non-container workloads such as VMs and serverless technologies. Implementing a resilient service discovery system involves creating a set of servers that maintain and facilitate service registry operations. You can achieve this by installing a service discovery system or using a managed service discovery service. diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 126cf11b8d..36c9061135 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -19,7 +19,7 @@ "path": "intro/usecases/what-is-service-discovery" }, { - "title": "What is a Service Mesh?", + "title": "Service Mesh", "path": "intro/usecases/what-is-a-service-mesh" } ] From e27d1eb12c575c1940d58fa5567b6879fe840ad3 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Mon, 8 Aug 2022 18:24:43 -0400 Subject: [PATCH 203/339] Update docs for peered transparent proxy --- .../registration/service-registration.mdx | 32 ++++++++++++++++--- .../docs/connect/transparent-proxy.mdx | 14 ++++---- website/content/docs/discovery/dns.mdx | 15 ++++----- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/website/content/docs/connect/registration/service-registration.mdx b/website/content/docs/connect/registration/service-registration.mdx index 66b1aa96f7..5b6e8ef598 100644 --- a/website/content/docs/connect/registration/service-registration.mdx +++ b/website/content/docs/connect/registration/service-registration.mdx @@ -115,6 +115,7 @@ proxy = { mode = "transparent" transparent_proxy = {} upstreams = [] +} ``` ```json @@ -167,7 +168,7 @@ You can configure the service mesh proxy to create listeners for upstream servic |`destination_name` | String value that specifies the name of the service or prepared query to route the service mesh to. The prepared query should be the name or the ID of the prepared query. | Required | None | | `destination_namespace` | String value that specifies the namespace containing the upstream service. | Optional | `default` | | `destination_peer` | String value that specifies the name of the peer cluster containing the upstream service. | Optional | None | -| `destination_partition` | String value that specifies the name of the admin partition containing the upstream service. | Optional | `default` | +| `destination_partition` | String value that specifies the name of the admin partition containing the upstream service. If `destination_peer` is set, `destination_partition` refers to the local admin partition in which the peering was established. | Optional | `default` | | `local_bind_port` | Integer value that specifies the port to bind a local listener to. The application will make outbound connections to the upstream from the local port. | Required | None | | `local_bind_address` | String value that specifies the address to bind a local listener to. The application will make outbound connections to the upstream service from the local bind address. | Optional | `127.0.0.1` | | `local_bind_socket_path` | String value that specifies the path at which to bind a Unix domain socket listener. The application will make outbound connections to the upstream from the local bind socket path.
This parameter conflicts with the `local_bind_port` or `local_bind_address` parameters.
Supported when using Envoy as a proxy. | Optional | None| @@ -195,7 +196,7 @@ local_bind_socket_path = "/tmp/redis_5678.sock" local_bind_socket_mode = "0700" mesh_gateway = { mode = "local" - } +} ``` ```json @@ -257,6 +258,29 @@ local_bind_port = 9090 } ``` + + + +```hcl +destination_peer = "cloud-services" +destination_partition = "finance" +destination_namespace = "default" +destination_type = "service" +destination_name = "api" +local_bind_port = 9090 +``` + +```json +{ + "destination_peer": "cloud-services", + "destination_partition": "finance", + "destination_namespace": "default", + "destination_type": "service", + "destination_name": "api", + "local_bind_port": 9090 +} +``` + ## Proxy Modes @@ -297,7 +321,7 @@ registrations](/docs/discovery/services#service-definition-parameter-case). - `dialed_directly` `(bool: false)` - Determines whether this proxy instance's IP address can be dialed directly by transparent proxies. Transparent proxies typically dial upstreams using the "virtual" tagged address, which load balances across instances. A database cluster with a leader is an example - where dialing individual instances can be helpful. + where dialing individual instances can be helpful. Cannot be used with upstreams which define a `destination_peer`. ~> **Note:** Dynamic routing rules such as failovers and redirects do not apply to services dialed directly. Additionally, the connection is proxied using a TCP proxy with a connection timeout of 5 seconds. @@ -521,7 +545,7 @@ services { "proxy": { "name": "service-2", "local_service_socket_path": "/tmp/socket_service_2" - ... + ... } } } diff --git a/website/content/docs/connect/transparent-proxy.mdx b/website/content/docs/connect/transparent-proxy.mdx index 1091091cfb..93bbadb231 100644 --- a/website/content/docs/connect/transparent-proxy.mdx +++ b/website/content/docs/connect/transparent-proxy.mdx @@ -29,8 +29,8 @@ Without transparent proxy, application owners need to: With transparent proxy: -1. Upstreams are inferred from service intentions, so no explicit configuration - is needed. +1. Upstreams are inferred from service intentions and/or imported services, + so no explicit configuration is needed. 1. Outbound connections pointing to a KubeDNS name "just work" — network rules redirect them through the proxy. 1. Inbound traffic is forced to go through the proxy to prevent unauthorized @@ -45,15 +45,15 @@ and only reaches intended destinations since the proxy can enforce security and Previously, service mesh users would need to explicitly define upstreams for a service as a local listener on the sidecar proxy, and dial the local listener to reach the appropriate upstream. Users would also have to set intentions to allow specific services to talk to one another. Transparent proxying reduces this duplication, by determining upstreams -implicitly from Service Intentions. Explicit upstreams are still supported in the [proxy service +implicitly from Service Intentions and imported services from a peer. Explicit upstreams are still supported in the [proxy service registration](/docs/connect/registration/service-registration) on VMs and via the [annotation](/docs/k8s/annotations-and-labels#consul-hashicorp-com-connect-service-upstreams) in Kubernetes. To support transparent proxying, Consul's CLI now has a command [`consul connect redirect-traffic`](/commands/connect/redirect-traffic) to redirect traffic through an inbound and -outbound listener on the sidecar. Consul also watches Service Intentions and configures the Envoy proxy with the appropriate -upstream IPs. If the default ACL policy is "allow", then Service Intentions are not required. In Consul on Kubernetes, -the traffic redirection command is automatically set up via an init container. +outbound listener on the sidecar. Consul also watches Service Intentions and imported services then configures the Envoy +proxy with the appropriate upstream IPs. If the default ACL policy is "allow", then Service Intentions are not required. +In Consul on Kubernetes, the traffic redirection command is automatically set up via an init container. ## Prerequisites @@ -62,7 +62,7 @@ the traffic redirection command is automatically set up via an init container. * To use transparent proxy on Kubernetes, Consul-helm >= `0.32.0` and Consul-k8s >= `0.26.0` are required in addition to Consul >= `1.10.0`. * If the default policy for ACLs is "deny", then Service Intentions should be set up to allow intended services to connect to each other. Otherwise, all Connect services can talk to all other services. -* If using Transparent Proxy, all worker nodes within a Kubernetes cluster must have the `ip_tables` kernel module running, e.g. `modprobe ip_tables`. +* If using Transparent Proxy, all worker nodes within a Kubernetes cluster must have the `ip_tables` kernel module running, e.g. `modprobe ip_tables`. The Kubernetes integration takes care of registering Kubernetes services with Consul, injecting a sidecar proxy, and enabling traffic redirection. diff --git a/website/content/docs/discovery/dns.mdx b/website/content/docs/discovery/dns.mdx index 471b9f9a20..f08a43c6ed 100644 --- a/website/content/docs/discovery/dns.mdx +++ b/website/content/docs/discovery/dns.mdx @@ -488,18 +488,17 @@ fields must be present: * `partition` * `datacenter` +For imported lookups, only the namespace and peer need to be specified as the partition can be inferred from the peering: + +```text +.virtual[.namespace][.peer]. +``` + For node lookups, only the partition and datacenter need to be specified as nodes cannot be namespaced. ```text -[tag.].service..ap..dc. -``` - -For node lookups, only the partition and datacenter need to be specified (nodes cannot be -namespaced): - -```text -[tag.].service..ap..dc. +[tag.].node..ap..dc. ``` ## DNS with ACLs From 407147b2e6408204bf57904956fe2e9b7c48c831 Mon Sep 17 00:00:00 2001 From: Tu Nguyen Date: Mon, 8 Aug 2022 16:40:14 -0700 Subject: [PATCH 204/339] Fixed rendering --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index c1acacc7eb..5bf7f501a9 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -64,6 +64,7 @@ Create a JSON file that contains the first cluster's name and the peering token. 1. In the **Generate token** tab, enter `cluster-02` in the **Name of peer** field. 1. Click the **Generate token** button. 1. Copy the token before you proceed. Be careful not to lose the token, as you cannot view the token again after leaving this screen. If you lose your token, you must generate a new one. +
@@ -89,6 +90,7 @@ When you connect server agents through cluster peering, they peer their default 1. Click **Establish peering**. 1. In the **Name of peer** field, enter `cluster-01`. Then paste the peering token in the **Token** field. 1. Click **Add peer**. + From edcca6e1a24d27828549bb9b9398e1dbe364e59d Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Mon, 8 Aug 2022 21:26:38 -0400 Subject: [PATCH 205/339] Apply Adam's suggestions Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/k8s/k8s-cli.mdx | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 4f6d86b324..907e7ac32b 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -8,10 +8,10 @@ description: >- # Consul on Kubernetes CLI Reference The Consul on Kubernetes CLI, `consul-k8s`, is a tool for managing Consul -without needing to directly interact with Helm, the [Consul CLI](/commands/index), +that does not require direct interaction with Helm, the [Consul CLI](/commands/index), or `kubectl`. --> **Tip:** For guidance on how to install `consul-k8s`, visit the +For guidance on how to install `consul-k8s`, refer to the [Installing the Consul K8s CLI](/docs/k8s/installation/install-cli) documentation. This topic describes the commands and available options for using `consul-k8s`. @@ -95,7 +95,7 @@ $ consul-k8s install -set-string key=value-bool ### `proxy` -The `proxy` command exposes two subcommands for interacting proxies managed by +The `proxy` command exposes two subcommands for interacting with proxies managed by Consul in your Kubernetes Cluster. - [`proxy list`](#proxy-list): List all Pods running proxies managed by Consul. @@ -115,22 +115,22 @@ $ consul-k8s proxy list Refer to the [Global Options](#global-options) for additional options that you can use when installing Consul on Kubernetes. -This command will list proxies alongside their `Type`. Types of proxies include: +This command lists proxies and their `Type`. Types of proxies include: -- `Sidecar`: these will be the majority of pods in the cluster. They run the - proxy in a sidecar pattern to network the pod as a service in the mesh. -- `API Gateway`: these pods run a proxy to manage connections with networks +- `Sidecar`: The majority of pods in the cluster are `Sidecar` types. They run the + proxy as a sidecar to connect the pod as a service in the mesh. +- `API Gateway`: These pods run a proxy to manage connections with networks outside of the Consul cluster. Read more about [API gateways](/docs/api-gateway). -- `Ingress Gateway`: these pods run a proxy to manage ingress into the +- `Ingress Gateway`: These pods run a proxy to manage ingress into the Kubernetes cluster. Read more about [ingress gateways](/docs/k8s/connect/ingress-gateways). -- `Terminating Gateway`: these pods run a proxy to control connections to +- `Terminating Gateway`: These pods run a proxy to control connections to external services. Read more about [terminating gateways](/docs/k8s/connect/terminating-gateways). -- `Mesh Gateway`: these pods run a proxy to manage connections between +- `Mesh Gateway`: These pods run a proxy to manage connections between Consul clusters connected using mesh federation. Read more about [Consul Mesh Federation](/docs/k8s/installation/multi-cluster/kubernetes). #### Example Commands -Display all pods in the current Kubernetes namespace which run proxies managed +Display all pods in the current Kubernetes namespace that run proxies managed by Consul. ```shell-session @@ -148,7 +148,7 @@ client-767ccfc8f9-ggrtx Sidecar frontend-676564547c-v2mfq Sidecar ``` -Display all pods in the `consul` Kubernetes namespace which run proxies managed +Display all pods in the `consul` Kubernetes namespace that run proxies managed by Consul. ```shell-session @@ -163,7 +163,7 @@ consul-ingress-gateway-6fb5544485-br6fl Ingress Gateway consul-ingress-gateway-6fb5544485-m54sp Ingress Gateway ``` -Display all pods across all namespaces which run proxies managed by Consul. +Display all Pods across all namespaces that run proxies managed by Consul. ```shell-session $ consul-k8s proxy list -A @@ -252,7 +252,7 @@ Name Type Last Updated ``` Get the Envoy configuration summary for all clusters with a fully qualified -domain name which includes `"default"`. Display only clusters and listeners. +domain name that includes `"default"`. Display only clusters and listeners. ```shell-session $ consul-k8s proxy read backend-658b679b45-d5xlb -fqdn default -clusters -listeners @@ -424,7 +424,7 @@ $ consul-k8s proxy read backend-658b679b45-d5xlb -o json ``` Get the raw Envoy configuration dump and clusters information for the Envoy -proxy running on the Pod `backend-658b679b45-d5xlb`. This command will return +proxy running on the Pod `backend-658b679b45-d5xlb`. The example command returns the raw configuration for each service as JSON. You can use the [JQ command line tool](https://stedolan.github.io/jq/) to index into the configuration for the service you want to inspect. @@ -449,7 +449,7 @@ $ consul-k8s proxy read backend-658b679b45-d5xlb -o raw ### `status` -The `status` command provides an overall status summary of the Consul on Kubernetes installation. It also provides the config that was used to deploy Consul K8s and provides a quick glance at the health of both Consul servers and clients. This command does not take in any flags. +The `status` command provides an overall status summary of the Consul on Kubernetes installation. It also provides the configuration that was used to deploy Consul K8s and information about the health of Consul servers and clients. This command does not take in any flags. ```shell-session $ consul-k8s status From e4a579022bbe14a5562e9e2a21f33c8ea75e8731 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Tue, 9 Aug 2022 11:08:07 +0100 Subject: [PATCH 206/339] ui: Add undefined check for peer model creation (#14075) --- ui/packages/consul-ui/app/services/repository/peer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/consul-ui/app/services/repository/peer.js b/ui/packages/consul-ui/app/services/repository/peer.js index 432b06fbbb..5f66708e2a 100644 --- a/ui/packages/consul-ui/app/services/repository/peer.js +++ b/ui/packages/consul-ui/app/services/repository/peer.js @@ -55,7 +55,7 @@ export default class PeerService extends RepositoryService { @dataSource('/:partition/:ns/:dc/peer-initiate/') @dataSource('/:partition/:ns/:dc/peer/:name') async fetchOne({ partition, ns, dc, name }, { uri }, request) { - if (name === '') { + if (typeof name === 'undefined' || name === '') { const item = this.create({ Datacenter: dc, Namespace: '', From 7854b6edc9050c015c4125b4d757fd0d7c180482 Mon Sep 17 00:00:00 2001 From: boruszak Date: Tue, 9 Aug 2022 08:53:03 -0500 Subject: [PATCH 207/339] Delete peering UI instructions --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index 5bf7f501a9..0efefb3b0d 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -244,7 +244,7 @@ $ curl http://127.0.0.1:8500/v1/peering/cluster-02 -In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in a datacenter. Click the name of a peered cluster to view additional details about the peering connection. +In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. Click the name of a peered cluster to view additional details about the peering connection. @@ -277,5 +277,9 @@ $ curl --request DELETE http://127.0.0.1:8500/v1/peering/cluster-02 +In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. + +Next to the name of the peer, click **More** (three horizontal dots) and then **Delete**. Click **Delete** to confirm and remove the peering connection. + From 2274f35c89ceb6001542da5b94551981a51cd1f8 Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:01:27 -0500 Subject: [PATCH 208/339] Update website/content/docs/connect/cluster-peering/create-manage-peering.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index 5bf7f501a9..b558813f41 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -15,7 +15,7 @@ A peering token enables cluster peering between different datacenters. Once you Cluster peering is not enabled by default on Consul servers. To peer clusters, you must first configure all Consul servers so that `peering` is `enabled`. For additional information, refer to [Configuration Files](/docs/agent/config/config-files). -Then, complete the following steps in order: +After enabling peering for all Consul servers, complete the following steps in order: 1. Create a peering token 1. Establish a connection between clusters From ce316568c9c81b7c80777d5a32fe726604759987 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Tue, 9 Aug 2022 10:01:30 -0400 Subject: [PATCH 209/339] Update wording on intentions --- website/content/docs/connect/transparent-proxy.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/connect/transparent-proxy.mdx b/website/content/docs/connect/transparent-proxy.mdx index 93bbadb231..6e3353bbad 100644 --- a/website/content/docs/connect/transparent-proxy.mdx +++ b/website/content/docs/connect/transparent-proxy.mdx @@ -29,8 +29,8 @@ Without transparent proxy, application owners need to: With transparent proxy: -1. Upstreams are inferred from service intentions and/or imported services, - so no explicit configuration is needed. +1. Local upstreams are inferred from service intentions and peered upstreams are + inferred from imported services, so no explicit configuration is needed. 1. Outbound connections pointing to a KubeDNS name "just work" — network rules redirect them through the proxy. 1. Inbound traffic is forced to go through the proxy to prevent unauthorized From 31434093f17143a50a2b9da10d704a2a7ff929f5 Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:01:42 -0500 Subject: [PATCH 210/339] Update website/content/docs/connect/cluster-peering/create-manage-peering.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index b558813f41..4192c9c810 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -30,7 +30,7 @@ The UI does not currently support exporting services between clusters or authori To begin the cluster peering process, generate a peering token in one of your clusters. The other cluster uses this token to establish the peering connection. -Everytime you generate a peering token, a single-use establishment secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. +Every time you generate a peering token, a single-use establishment secret is embedded in the token. Because regenerating a peering token invalidates the previously generated secret, you must use the most recently created token to establish peering connections. From 77e4a5f3c48b3c06af0cb71163c423d38fa79202 Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:02:05 -0500 Subject: [PATCH 211/339] Update website/content/docs/connect/cluster-peering/create-manage-peering.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index 4192c9c810..e0621e2965 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -164,7 +164,7 @@ $ consul config write peering-intentions.hcl ## Manage peering connections -After you establish a peering connection, you can get a list of all active peering connections, read a specific peering connection's info, check peering connection health, and delete peering connections. +After you establish a peering connection, you can get a list of all active peering connections, read a specific peering connection's information, check peering connection health, and delete peering connections. ### List all peering connections From 6840861a17dfaecf2229019c8dd7580763b16527 Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:02:16 -0500 Subject: [PATCH 212/339] Update website/content/docs/connect/cluster-peering/create-manage-peering.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index e0621e2965..5cad250c9e 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -173,7 +173,7 @@ You can list all active peering connections in a cluster. -After you establish a peering connection, [query the `/peering/` endpoint](/api-docs/peering#list-all-peerings) to get a list of all peering connections. For example, the following command requests a list of all peering connections on `localhost` and returns the info as a series of JSON objects: +After you establish a peering connection, [query the `/peering/` endpoint](/api-docs/peering#list-all-peerings) to get a list of all peering connections. For example, the following command requests a list of all peering connections on `localhost` and returns the information as a series of JSON objects: ```shell-session $ curl http://127.0.0.1:8500/v1/peerings From b019ce6e622077ee4d8816498734d890d03f658a Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:02:33 -0500 Subject: [PATCH 213/339] Update website/content/docs/connect/cluster-peering/create-manage-peering.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index 5cad250c9e..713b6eb347 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -222,7 +222,7 @@ You can get information about individual peering connections between clusters. -After you establish a peering connection, [query the `/peering/:name` endpoint](/api-docs/peering#read-a-peering-connection) to get peering information about for a specific cluster. For example, the following command requests peering connection info for "cluster-02" and returns the info as a JSON object: +After you establish a peering connection, [query the `/peering/:name` endpoint](/api-docs/peering#read-a-peering-connection) to get peering information about for a specific cluster. For example, the following command requests peering connection information for "cluster-02" and returns the info as a JSON object: ```shell-session $ curl http://127.0.0.1:8500/v1/peering/cluster-02 From 9c2d7fb2685828a2c0f075f8401b273530dfe9ea Mon Sep 17 00:00:00 2001 From: boruszak Date: Tue, 9 Aug 2022 09:07:25 -0500 Subject: [PATCH 214/339] WAN Federation/Cluster Peering comparison table addition --- .../docs/connect/cluster-peering/index.mdx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/index.mdx b/website/content/docs/connect/cluster-peering/index.mdx index bfce6dbd03..7b1c9c6842 100644 --- a/website/content/docs/connect/cluster-peering/index.mdx +++ b/website/content/docs/connect/cluster-peering/index.mdx @@ -27,14 +27,16 @@ WAN federation and cluster peering are different ways to connect clusters. The m Regardless of whether you connect your clusters through WAN federation or cluster peering, human and machine users can use either method to discover services in other clusters or dial them through the service mesh. -| | WAN Federation | Cluster Peering | -| :----------------------------------------------- | :------------: | :-------------: | -| Connects clusters across datacenters | ✅ | ✅ | -| Shares support queries and service endpoints | ✅ | ✅ | -| Connects clusters owned by different operators | ❌ | ✅ | -| Functions without declaring primary datacenter | ❌ | ✅ | -| Shares key/value stores | ✅ | ❌ | -| Uses gossip protocol | ✅ | ❌ | +| | WAN Federation | Cluster Peering | +| :------------------------------------------------- | :------------: | :-------------: | +| Connects clusters across datacenters | ✅ | ✅ | +| Shares support queries and service endpoints | ✅ | ✅ | +| Connects clusters owned by different operators | ❌ | ✅ | +| Functions without declaring primary datacenter | ❌ | ✅ | +| Replicates exported services for service discovery | ❌ | ✅ | +| Shares key/value stores | ✅ | ❌ | +| Uses gossip protocol | ✅ | ❌ | +| Forwards service requests for service discovery | ✅ | ❌ | ## Beta release features and constraints From c3bebf80ce2c71caa7f2a7a9aeaf9658c6d22823 Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:10:34 -0500 Subject: [PATCH 215/339] Update website/content/docs/connect/cluster-peering/index.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/connect/cluster-peering/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/index.mdx b/website/content/docs/connect/cluster-peering/index.mdx index bfce6dbd03..6f522b708c 100644 --- a/website/content/docs/connect/cluster-peering/index.mdx +++ b/website/content/docs/connect/cluster-peering/index.mdx @@ -40,7 +40,7 @@ Regardless of whether you connect your clusters through WAN federation or cluste The cluster peering beta includes the following features and functionality: -- Mesh Gateways for _service to service traffic_ between clusters are available. For more information on configuring mesh gateways across peers, refer to [Service-to-service Traffic Across Peered Clusters](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers). +- Mesh gateways for _service to service traffic_ between clusters are available. For more information on configuring mesh gateways across peers, refer to [Service-to-service Traffic Across Peered Clusters](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers). - You can generate peering tokens, establish, list, read, and delete peerings, and manage intentions for peering connections with both the API and the UI. - You can configure [transparent proxies](/docs/connect/transparent-proxy) for peered services. - You can use the [`peering` rule for ACL enforcement](/docs/security/acl/acl-rules#peering) of peering APIs. From fc010e3fe23e1d6060f7de380cd3c9e8f4085cc7 Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Tue, 9 Aug 2022 09:25:45 -0500 Subject: [PATCH 216/339] Update website/content/docs/connect/cluster-peering/k8s.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/connect/cluster-peering/k8s.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index 3a43d0c034..69b34e2e54 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -11,7 +11,7 @@ description: >- with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support. -To establish a cluster peering connection on Kubernetes, you need to enable the feature in the Helm chart and create Custom Resource Definitions (CRDs) for each side of the peering. +To establish a cluster peering connection on Kubernetes, you need to enable the feature in the Helm chart and create custom resource definitions (CRDs) for each side of the peering. The following CRDs are used to create and manage a peering connection: From 166c95e40f4d7d7e76d900ba7fd7f7260425226c Mon Sep 17 00:00:00 2001 From: boruszak Date: Tue, 9 Aug 2022 09:42:01 -0500 Subject: [PATCH 217/339] Fixes according to Freddy's review/comments --- website/content/docs/connect/cluster-peering/index.mdx | 5 +++-- .../mesh-gateway/service-to-service-traffic-peers.mdx | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/index.mdx b/website/content/docs/connect/cluster-peering/index.mdx index 7b1c9c6842..a04b458c34 100644 --- a/website/content/docs/connect/cluster-peering/index.mdx +++ b/website/content/docs/connect/cluster-peering/index.mdx @@ -34,9 +34,9 @@ Regardless of whether you connect your clusters through WAN federation or cluste | Connects clusters owned by different operators | ❌ | ✅ | | Functions without declaring primary datacenter | ❌ | ✅ | | Replicates exported services for service discovery | ❌ | ✅ | +| Forwards service requests for service discovery | ✅ | ❌ | | Shares key/value stores | ✅ | ❌ | | Uses gossip protocol | ✅ | ❌ | -| Forwards service requests for service discovery | ✅ | ❌ | ## Beta release features and constraints @@ -50,9 +50,10 @@ The cluster peering beta includes the following features and functionality: Not all features and functionality are available in the beta release. In particular, consider the following technical constraints: - Mesh gateways for _server to server traffic_ are not available. +- Services with node, instance, and check definitions totaling more than 4MB cannot be exported to a peer. - Dynamic routing features such as splits, custom routes, and redirects cannot target services in a peered cluster. - Configuring service failover across peers is not supported for service mesh. - Consul datacenters that are already federated stay federated. You do not need to migrate WAN federated clusters to cluster peering. - The `consul intention` CLI command is not supported. To manage intentions that specify services in peered clusters, use [configuration entries](/docs/connect/config-entries/service-intentions). - Accessing key/value stores across peers is not supported. -- Non-enterprise Consul instances cannot sync services with namespaces outside of the `default` namespace. +- Because non-Enterprise Consul instances are restricted to the `default` namespace, Consul Enterprise instances cannot export services from outside of the `default` namespace to non-Enterprise peers. diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx index 1de6e019a2..34cba6306b 100644 --- a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx @@ -12,7 +12,7 @@ description: >- Mesh gateways are required for you to route service mesh traffic between different Consul clusters. Clusters can reside in different clouds or runtime environments where general interconnectivity between all services in all clusters is not feasible. -Unlike mesh gateways for datacenters and partitions, mesh gateways for cluster peering decrypts data to HTTP services within the mTLS session. Data must be decrypted in order to apply dynamic routing rules configured in the destination cluster. +Unlike mesh gateways for datacenters and partitions, mesh gateways for cluster peering decrypt data to HTTP services within the mTLS session. Data must be decrypted in order to evaluate and apply dynamic routing rules at the destination cluster, which reduces coupling between peers. ## Prerequisites @@ -21,6 +21,7 @@ To configure mesh gateways for cluster peering, make sure your Consul environmen - Consul version 1.13.0 or newer. - A local Consul agent is required to manage mesh gateway configuration. - [Enable Consul service mesh](/docs/agent/config/config-files#connect-parameters) in all clusters. +- [Enable `peering`](/docs/agent/config/config-files) on all Consul servers. - Use [Envoy proxies](/docs/connect/proxies/envoy). Envoy is the only proxy with mesh gateway capabilities in Consul. ## Configuration From fa462915c9e1f09116998da1e1eba428f4d837dc Mon Sep 17 00:00:00 2001 From: boruszak Date: Tue, 9 Aug 2022 09:42:22 -0500 Subject: [PATCH 218/339] Addt'l fixes --- .../gateways/mesh-gateway/service-to-service-traffic-peers.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx index 34cba6306b..0163730ba4 100644 --- a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-peers.mdx @@ -52,4 +52,4 @@ Alternatively, you can also use the CLI to spin up and register a gateway in Con ### Modes -In the current release, modes are not configurable for mesh gateways that connect peered clusters. By default, all proxies connected to the gateway behave in [remote mode](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters#remote). +Modes are not configurable for mesh gateways that connect peered clusters. By default, all proxies connecting to peered clusters use mesh gateways in [remote mode](/docs/connect/gateways/mesh-gateway/service-to-service-traffic-datacenters#remote). From 1d184a08435fe5e938a3d7cfb121a03645bc09fa Mon Sep 17 00:00:00 2001 From: boruszak Date: Tue, 9 Aug 2022 10:09:53 -0500 Subject: [PATCH 219/339] Fixes --- .../cluster-peering/create-manage-peering.mdx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index fc0f946a4f..5662868834 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -35,7 +35,7 @@ Every time you generate a peering token, a single-use establishment secret is em -In `cluster-01`, issue a request for a peering token. +In `cluster-01`, use the [`/peering/token` endpoint](/api-docs/peering#generate-a-peering-token) to issue a request for a peering token. ```shell-session $ curl --request POST --data '{"PeerName":"cluster-02"}' --url http://localhost:8500/v1/peering/token @@ -75,13 +75,17 @@ Next, use the peering token to establish a secure connection between the cluster -In one of the client agents in "cluster-02," use `peering_token.json` to establish the peering connection. This endpoint does not generate an output unless there is an error. +In one of the client agents in "cluster-02," use `peering_token.json` and the [`/peering/establish` endpoint](/api-docs/peering#establish-a-peering-connection) to establish the peering connection. This endpoint does not generate an output unless there is an error. ```shell-session $ curl --request POST --data @peering_token.json http://127.0.0.1:8500/v1/peering/establish ``` When you connect server agents through cluster peering, they peer their default partitions. To establish peering connections for other partitions through server agents, you must add the `Partition` field to `peering_token.json` and specify the partitions you want to peer. For additional configuration information, refer to [Cluster Peering - HTTP API](/api-docs/peering). + + +You can dial the `peering/establish` endpoint once per peering token. Peering tokens cannot be reused after being used to establish a connection. If you need to re-establish a connection, you must generate a new peering token. + @@ -118,6 +122,7 @@ Services = [ ## during the peering process. Peer = "cluster-02" } + } ] ``` @@ -173,7 +178,7 @@ You can list all active peering connections in a cluster. -After you establish a peering connection, [query the `/peering/` endpoint](/api-docs/peering#list-all-peerings) to get a list of all peering connections. For example, the following command requests a list of all peering connections on `localhost` and returns the information as a series of JSON objects: +After you establish a peering connection, [query the `/peerings/` endpoint](/api-docs/peering#list-all-peerings) to get a list of all peering connections. For example, the following command requests a list of all peering connections on `localhost` and returns the information as a series of JSON objects: ```shell-session $ curl http://127.0.0.1:8500/v1/peerings @@ -222,7 +227,7 @@ You can get information about individual peering connections between clusters. -After you establish a peering connection, [query the `/peering/:name` endpoint](/api-docs/peering#read-a-peering-connection) to get peering information about for a specific cluster. For example, the following command requests peering connection information for "cluster-02" and returns the info as a JSON object: +After you establish a peering connection, [query the `/peering/` endpoint](/api-docs/peering#read-a-peering-connection) to get peering information about for a specific cluster. For example, the following command requests peering connection information for "cluster-02" and returns the info as a JSON object: ```shell-session $ curl http://127.0.0.1:8500/v1/peering/cluster-02 From 6311c651dea98f4e2684551beb3ea143ae7f6d6d Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Tue, 9 Aug 2022 10:36:47 -0400 Subject: [PATCH 220/339] Add retry in TestAgentConnectCALeafCert_good --- agent/agent_endpoint_test.go | 45 +++++++++++++++--------------------- agent/config/config.go | 2 +- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 270cc7dc13..67850f9ebd 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -6799,7 +6799,7 @@ func TestAgentConnectCALeafCert_good(t *testing.T) { ca2 := connect.TestCAConfigSet(t, a, nil) // Issue a blocking query to ensure that the cert gets updated appropriately - { + t.Run("test blocking queries update leaf cert", func(t *testing.T) { resp := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/v1/agent/connect/ca/leaf/test?index="+index, nil) a.srv.h.ServeHTTP(resp, req) @@ -6815,7 +6815,7 @@ func TestAgentConnectCALeafCert_good(t *testing.T) { // Should not be a cache hit! The data was updated in response to the blocking // query being made. require.Equal(t, "MISS", resp.Header().Get("X-Cache")) - } + }) t.Run("test non-blocking queries update leaf cert", func(t *testing.T) { resp := httptest.NewRecorder() @@ -6834,33 +6834,26 @@ func TestAgentConnectCALeafCert_good(t *testing.T) { // Set a new CA ca3 := connect.TestCAConfigSet(t, a, nil) - resp := httptest.NewRecorder() req, err := http.NewRequest("GET", "/v1/agent/connect/ca/leaf/test", nil) require.NoError(t, err) - obj, err = a.srv.AgentConnectCALeafCert(resp, req) - require.NoError(t, err) - issued2 := obj.(*structs.IssuedCert) - require.NotEqual(t, issued.CertPEM, issued2.CertPEM) - require.NotEqual(t, issued.PrivateKeyPEM, issued2.PrivateKeyPEM) - // Verify that the cert is signed by the new CA - requireLeafValidUnderCA(t, issued2, ca3) - - // Should not be a cache hit! - require.Equal(t, "MISS", resp.Header().Get("X-Cache")) - } - - // Test caching for the leaf cert - { - - for fetched := 0; fetched < 4; fetched++ { - - // Fetch it again + retry.Run(t, func(r *retry.R) { resp := httptest.NewRecorder() - obj2, err := a.srv.AgentConnectCALeafCert(resp, req) - require.NoError(t, err) - require.Equal(t, obj, obj2) - } + a.srv.h.ServeHTTP(resp, req) + + // Should not be a cache hit! + require.Equal(r, "MISS", resp.Header().Get("X-Cache")) + + dec := json.NewDecoder(resp.Body) + issued2 := &structs.IssuedCert{} + require.NoError(r, dec.Decode(issued2)) + + require.NotEqual(r, issued.CertPEM, issued2.CertPEM) + require.NotEqual(r, issued.PrivateKeyPEM, issued2.PrivateKeyPEM) + + // Verify that the cert is signed by the new CA + requireLeafValidUnderCA(r, issued2, ca3) + }) } }) } @@ -7405,7 +7398,7 @@ func waitForActiveCARoot(t *testing.T, srv *HTTPHandlers, expect *structs.CARoot }) } -func requireLeafValidUnderCA(t *testing.T, issued *structs.IssuedCert, ca *structs.CARoot) { +func requireLeafValidUnderCA(t require.TestingT, issued *structs.IssuedCert, ca *structs.CARoot) { leaf, intermediates, err := connect.ParseLeafCerts(issued.CertPEM) require.NoError(t, err) diff --git a/agent/config/config.go b/agent/config/config.go index ca6900b51f..145c74db7c 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -611,7 +611,7 @@ type Connect struct { MeshGatewayWANFederationEnabled *bool `mapstructure:"enable_mesh_gateway_wan_federation"` EnableServerlessPlugin *bool `mapstructure:"enable_serverless_plugin"` - // TestCALeafRootChangeSpread controls how long after a CA roots change before new leaft certs will be generated. + // TestCALeafRootChangeSpread controls how long after a CA roots change before new leaf certs will be generated. // This is only tuned in tests, generally set to 1ns to make tests deterministic with when to expect updated leaf // certs by. This configuration is not exposed to users (not documented, and agent/config/default.go will override it) TestCALeafRootChangeSpread *string `mapstructure:"test_ca_leaf_root_change_spread"` From e3046120b359d48e1b32f2e9d977571cd5f3068f Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Tue, 9 Aug 2022 12:22:39 -0400 Subject: [PATCH 222/339] Close active listeners on error If startListeners successfully created listeners for some of its input addresses but eventually failed, the function would return an error and existing listeners would not be cleaned up. --- .changelog/14081.txt | 3 ++ agent/agent.go | 19 +++++++++++-- agent/agent_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 .changelog/14081.txt diff --git a/.changelog/14081.txt b/.changelog/14081.txt new file mode 100644 index 0000000000..ccb03ffb03 --- /dev/null +++ b/.changelog/14081.txt @@ -0,0 +1,3 @@ +```release-note:bug +agent: Fixes an issue where an agent that fails to start due to bad addresses won't clean up any existing listeners +``` diff --git a/agent/agent.go b/agent/agent.go index e087af5187..8a263647e9 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -863,8 +863,18 @@ func (a *Agent) listenAndServeDNS() error { return merr.ErrorOrNil() } +// startListeners will return a net.Listener for every address unless an +// error is encountered, in which case it will close all previously opened +// listeners and return the error. func (a *Agent) startListeners(addrs []net.Addr) ([]net.Listener, error) { - var ln []net.Listener + var lns []net.Listener + + closeAll := func() { + for _, l := range lns { + l.Close() + } + } + for _, addr := range addrs { var l net.Listener var err error @@ -873,22 +883,25 @@ func (a *Agent) startListeners(addrs []net.Addr) ([]net.Listener, error) { case *net.UnixAddr: l, err = a.listenSocket(x.Name) if err != nil { + closeAll() return nil, err } case *net.TCPAddr: l, err = net.Listen("tcp", x.String()) if err != nil { + closeAll() return nil, err } l = &tcpKeepAliveListener{l.(*net.TCPListener)} default: + closeAll() return nil, fmt.Errorf("unsupported address type %T", addr) } - ln = append(ln, l) + lns = append(lns, l) } - return ln, nil + return lns, nil } // listenHTTP binds listeners to the provided addresses and also returns diff --git a/agent/agent_test.go b/agent/agent_test.go index d7b118fcba..8bae81ce45 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -5857,6 +5857,73 @@ func Test_coalesceTimerTwoPeriods(t *testing.T) { } +func TestAgent_startListeners(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + t.Parallel() + + ports := freeport.GetN(t, 3) + bd := BaseDeps{ + Deps: consul.Deps{ + Logger: hclog.NewInterceptLogger(nil), + Tokens: new(token.Store), + GRPCConnPool: &fakeGRPCConnPool{}, + }, + RuntimeConfig: &config.RuntimeConfig{ + HTTPAddrs: []net.Addr{}, + }, + Cache: cache.New(cache.Options{}), + } + + bd, err := initEnterpriseBaseDeps(bd, nil) + require.NoError(t, err) + + agent, err := New(bd) + require.NoError(t, err) + + // use up an address + used := net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[2]} + l, err := net.Listen("tcp", used.String()) + require.NoError(t, err) + t.Cleanup(func() { l.Close() }) + + var lns []net.Listener + t.Cleanup(func() { + for _, ln := range lns { + ln.Close() + } + }) + + // first two addresses open listeners but third address should fail + lns, err = agent.startListeners([]net.Addr{ + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[0]}, + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[1]}, + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[2]}, + }) + require.Contains(t, err.Error(), "address already in use") + + // first two ports should be freed up + retry.Run(t, func(r *retry.R) { + lns, err = agent.startListeners([]net.Addr{ + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[0]}, + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[1]}, + }) + require.NoError(r, err) + require.Len(r, lns, 2) + }) + + // first two ports should be in use + retry.Run(t, func(r *retry.R) { + _, err = agent.startListeners([]net.Addr{ + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[0]}, + &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[1]}, + }) + require.Contains(r, err.Error(), "address already in use") + }) + +} + func getExpectedCaPoolByFile(t *testing.T) *x509.CertPool { pool := x509.NewCertPool() data, err := ioutil.ReadFile("../test/ca/root.cer") From b56fb1aa4454b644aafe41d9fe5cbfefa04b0268 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Tue, 9 Aug 2022 12:25:33 -0400 Subject: [PATCH 223/339] Remove the beta warning for the upgrade command --- website/content/docs/k8s/k8s-cli.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/content/docs/k8s/k8s-cli.mdx b/website/content/docs/k8s/k8s-cli.mdx index 907e7ac32b..8f6467cbb0 100644 --- a/website/content/docs/k8s/k8s-cli.mdx +++ b/website/content/docs/k8s/k8s-cli.mdx @@ -523,8 +523,6 @@ $ consul-k8s uninstall -namespace=my-ns -name=my-consul -wipe-data=true -auto-ap ### `upgrade` --> The `consul-k8s upgrade` **subcommand is currently in beta**: This subcommand is not recommended for production environments. - The `upgrade` command upgrades the Consul on Kubernetes components to the current version of the `consul-k8s` cli. Prior to running `consul-k8s upgrade`, the `consul-k8s` CLI should first be upgraded to the latest version as described [Upgrade the Consul K8s CLI](#upgrade-the-consul-k8s-cli) ```shell-session From 807898a3c2bdac9890ffaac61fc52b4cc855086a Mon Sep 17 00:00:00 2001 From: Mike Nomitch Date: Thu, 28 Jul 2022 09:18:57 -0700 Subject: [PATCH 225/339] Updates upgrade docs to clarify Nomad bug is fixed --- website/content/docs/upgrading/upgrade-specific.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/content/docs/upgrading/upgrade-specific.mdx b/website/content/docs/upgrading/upgrade-specific.mdx index ec0cf54d5a..bf8c731259 100644 --- a/website/content/docs/upgrading/upgrade-specific.mdx +++ b/website/content/docs/upgrading/upgrade-specific.mdx @@ -50,9 +50,11 @@ style `consul.api.http...` metrics and removing the configuration flag from your ### Nomad Namespace Incompatibility -Nomad Enterprise users should not upgrade to Consul Enterprise 1.12.0. +Nomad Enterprise users should not upgrade to Consul Enterprise 1.12.0, and instead should upgrade to 1.12.1 or later. -Consul 1.12.0 Enterprise introduced a change that prevents Nomad Enterprise from removing services from non-default Consul namespaces. To avoid errors, we recommend that Nomad Enterprise users wait to update Consul Enterprise until we fix this issue in a future release. +Consul 1.12.0 Enterprise introduced a change that prevents Nomad Enterprise from removing services from non-default Consul namespaces. + +The Consul Enterprise codebase was updated with a fix for this issue in version 1.12.1. ### TLS Configuration From 89a70c61f5bf28e31cc9b7921c93068ce798e580 Mon Sep 17 00:00:00 2001 From: Michael Wilkerson <62034708+wilkermichael@users.noreply.github.com> Date: Tue, 9 Aug 2022 13:45:10 -0700 Subject: [PATCH 227/339] update docs (#13909) --- website/content/docs/nia/configuration.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/nia/configuration.mdx b/website/content/docs/nia/configuration.mdx index 2576a9fa0f..c8ae16e123 100644 --- a/website/content/docs/nia/configuration.mdx +++ b/website/content/docs/nia/configuration.mdx @@ -102,7 +102,7 @@ To read more on suggestions for configuring the Consul agent, see [run an agent] ```hcl consul { - address = "consul.example.com" + address = "localhost:8500" auth {} tls {} token = null From 6e0de48e603388da946cb68a802e5f734cedbfdd Mon Sep 17 00:00:00 2001 From: Daniel Upton Date: Tue, 9 Aug 2022 12:54:49 +0100 Subject: [PATCH 228/339] cli: update agent log preamble to reflect per-listener TLS config --- command/agent/agent.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index a69e630711..cc08213e1c 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -201,24 +201,29 @@ func (c *cmd) run(args []string) int { if config.ServerMode { segment = "" } - ui.Info(fmt.Sprintf(" Version: '%s'", c.versionHuman)) + ui.Info(fmt.Sprintf(" Version: '%s'", c.versionHuman)) if strings.Contains(c.versionHuman, "dev") { - ui.Info(fmt.Sprintf(" Revision: '%s'", c.revision)) + ui.Info(fmt.Sprintf(" Revision: '%s'", c.revision)) } - ui.Info(fmt.Sprintf(" Build Date: '%s'", c.buildDate)) - ui.Info(fmt.Sprintf(" Node ID: '%s'", config.NodeID)) - ui.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName)) + ui.Info(fmt.Sprintf(" Build Date: '%s'", c.buildDate)) + ui.Info(fmt.Sprintf(" Node ID: '%s'", config.NodeID)) + ui.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName)) if ap := config.PartitionOrEmpty(); ap != "" { - ui.Info(fmt.Sprintf(" Partition: '%s'", ap)) + ui.Info(fmt.Sprintf(" Partition: '%s'", ap)) } - ui.Info(fmt.Sprintf(" Datacenter: '%s' (Segment: '%s')", config.Datacenter, segment)) - ui.Info(fmt.Sprintf(" Server: %v (Bootstrap: %v)", config.ServerMode, config.Bootstrap)) - ui.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, gRPC: %d, DNS: %d)", config.ClientAddrs, + ui.Info(fmt.Sprintf(" Datacenter: '%s' (Segment: '%s')", config.Datacenter, segment)) + ui.Info(fmt.Sprintf(" Server: %v (Bootstrap: %v)", config.ServerMode, config.Bootstrap)) + ui.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, gRPC: %d, DNS: %d)", config.ClientAddrs, config.HTTPPort, config.HTTPSPort, config.GRPCPort, config.DNSPort)) - ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddrLAN, + ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddrLAN, config.SerfPortLAN, config.SerfPortWAN)) - ui.Info(fmt.Sprintf(" Encrypt: Gossip: %v, TLS-Outgoing: %v, TLS-Incoming: %v, Auto-Encrypt-TLS: %t", - config.EncryptKey != "", config.TLS.InternalRPC.VerifyOutgoing, config.TLS.InternalRPC.VerifyIncoming, config.AutoEncryptTLS || config.AutoEncryptAllowTLS)) + ui.Info(fmt.Sprintf("Gossip Encryption: %t", config.EncryptKey != "")) + ui.Info(fmt.Sprintf(" Auto-Encrypt-TLS: %t", config.AutoEncryptTLS || config.AutoEncryptAllowTLS)) + ui.Info(fmt.Sprintf(" HTTPS TLS: Verify Incoming: %t, Verify Outgoing: %t, Min Version: %s", + config.TLS.HTTPS.VerifyIncoming, config.TLS.HTTPS.VerifyOutgoing, config.TLS.HTTPS.TLSMinVersion)) + ui.Info(fmt.Sprintf(" gRPC TLS: Verify Incoming: %t, Min Version: %s", config.TLS.GRPC.VerifyIncoming, config.TLS.GRPC.TLSMinVersion)) + ui.Info(fmt.Sprintf(" Internal RPC TLS: Verify Incoming: %t, Verify Outgoing: %t (Verify Hostname: %t), Min Version: %s", + config.TLS.InternalRPC.VerifyIncoming, config.TLS.InternalRPC.VerifyOutgoing, config.TLS.InternalRPC.VerifyServerHostname, config.TLS.InternalRPC.TLSMinVersion)) // Enable log streaming ui.Output("") ui.Output("Log data will now stream in as it occurs:\n") From c5865d982e62fcf6fb2aa3180b7023dd06845cf2 Mon Sep 17 00:00:00 2001 From: boruszak Date: Wed, 10 Aug 2022 09:48:18 -0500 Subject: [PATCH 229/339] Not available on HCP Consul update --- .../docs/connect/cluster-peering/create-manage-peering.mdx | 2 +- website/content/docs/connect/cluster-peering/index.mdx | 2 +- website/content/docs/connect/cluster-peering/k8s.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index 5662868834..009c60f409 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -7,7 +7,7 @@ description: >- # Create and Manage Peering Connections -~> **Cluster peering is currently in beta:** Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support. +~> **Cluster peering is currently in beta:** Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. A peering token enables cluster peering between different datacenters. Once you generate a peering token, you can use it to establish a connection between clusters. Then you can export services and create intentions so that peered clusters can call those services. diff --git a/website/content/docs/connect/cluster-peering/index.mdx b/website/content/docs/connect/cluster-peering/index.mdx index 317fe320a4..8c4be1ce4c 100644 --- a/website/content/docs/connect/cluster-peering/index.mdx +++ b/website/content/docs/connect/cluster-peering/index.mdx @@ -7,7 +7,7 @@ description: >- # What is Cluster Peering? -~> **Cluster peering is currently in beta**: Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support. +~> **Cluster peering is currently in beta**: Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. You can create peering connections between two or more independent clusters so that services deployed to different partitions or datacenters can communicate. diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index 69b34e2e54..77ac6d38a2 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -9,7 +9,7 @@ description: >- ~> **Cluster peering is currently in beta:** Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in -beta may have performance issues, scaling issues, and limited support. +beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. To establish a cluster peering connection on Kubernetes, you need to enable the feature in the Helm chart and create custom resource definitions (CRDs) for each side of the peering. From 11e7a0d5471498df2b43d1b64c5492b8149faa22 Mon Sep 17 00:00:00 2001 From: cskh Date: Wed, 10 Aug 2022 10:53:57 -0400 Subject: [PATCH 230/339] fix: shadowed err in retryJoin() (#14112) - err value will be used later to surface the error message if r.join() returns any err. --- agent/retry_join.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/retry_join.go b/agent/retry_join.go index 8cfb00e22e..b807697e84 100644 --- a/agent/retry_join.go +++ b/agent/retry_join.go @@ -226,7 +226,8 @@ func (r *retryJoiner) retryJoin() error { for { addrs := retryJoinAddrs(disco, r.variant, r.cluster, r.addrs, r.logger) if len(addrs) > 0 { - n, err := r.join(addrs) + n := 0 + n, err = r.join(addrs) if err == nil { if r.variant == retryJoinMeshGatewayVariant { r.logger.Info("Refreshing mesh gateways completed") From e940842476770329a4ea2bea872ab766b1cf673a Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Wed, 10 Aug 2022 10:21:20 -0500 Subject: [PATCH 231/339] Update website/content/docs/connect/cluster-peering/index.mdx Co-authored-by: Tu Nguyen --- website/content/docs/connect/cluster-peering/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/index.mdx b/website/content/docs/connect/cluster-peering/index.mdx index 8c4be1ce4c..c4b8c7a4ee 100644 --- a/website/content/docs/connect/cluster-peering/index.mdx +++ b/website/content/docs/connect/cluster-peering/index.mdx @@ -7,7 +7,7 @@ description: >- # What is Cluster Peering? -~> **Cluster peering is currently in beta**: Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. +~> **Cluster peering is currently in beta**: Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. You can create peering connections between two or more independent clusters so that services deployed to different partitions or datacenters can communicate. From 26dff8e493e1e4fd8162489ff0c31197c082edee Mon Sep 17 00:00:00 2001 From: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> Date: Wed, 10 Aug 2022 10:21:26 -0500 Subject: [PATCH 232/339] Update website/content/docs/connect/cluster-peering/k8s.mdx Co-authored-by: Tu Nguyen --- website/content/docs/connect/cluster-peering/k8s.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index 77ac6d38a2..8b178ad9c7 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -9,7 +9,7 @@ description: >- ~> **Cluster peering is currently in beta:** Functionality associated with cluster peering is subject to change. You should never use the beta release in secure environments or production scenarios. Features in -beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. +beta may have performance issues, scaling issues, and limited support.

Cluster peering is not currently available in the HCP Consul offering. To establish a cluster peering connection on Kubernetes, you need to enable the feature in the Helm chart and create custom resource definitions (CRDs) for each side of the peering. From 3286a60a5f0e9cf8ca7cf735330c9d30dd838e15 Mon Sep 17 00:00:00 2001 From: Ashwin Venkatesh Date: Wed, 10 Aug 2022 13:14:36 -0400 Subject: [PATCH 233/339] Add docs to recreate peering token. --- website/content/docs/connect/cluster-peering/k8s.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index 8b178ad9c7..a3716d3d71 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -237,3 +237,13 @@ To confirm that you deleted your peering connection, in `cluster-01`, query the ```shell-session $ curl "localhost:8500/v1/health/connect/backend?peer=cluster-02" ``` + +## Recreate/Reset a peering connection + +To recreate or reset the peering connection, a new peering token needs to be generated on the cluster where the `PeeringAcceptor` was created, which in this case is `cluster-01`. + +This can be performed by creating/updating the annotation `consul.hashicorp.com/peering-version` on the `PeeringAcceptor`. If the annotation already exists, update its value to a version that is higher. + +Once the above is done, repeat the steps in the peering process from saving your peering token so that you can export it to the other cluster. This will re-establish peering with the updated token. + +-> **NOTE:** A new peering token is only generated upon manually setting and updating the value of the annotation `consul.hashicorp.com/peering-version`. Creating a new token will cause the previous token to expire. From c30fce54c633c702ab6d42312c01d8884a677d1d Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 10 Aug 2022 20:02:43 +0200 Subject: [PATCH 234/339] Use actual intention for permission check intentions edit (#14113) --- ui/packages/consul-ui/app/templates/dc/intentions/edit.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/consul-ui/app/templates/dc/intentions/edit.hbs b/ui/packages/consul-ui/app/templates/dc/intentions/edit.hbs index 2139fd7318..2affe93b28 100644 --- a/ui/packages/consul-ui/app/templates/dc/intentions/edit.hbs +++ b/ui/packages/consul-ui/app/templates/dc/intentions/edit.hbs @@ -22,7 +22,7 @@ as |route|> {{#let loader.data - (not (can "write intention" item=item)) + (not (can "write intention" item=loader.data)) as |item readOnly|}} From 9349bbb7060646aea99fae03711ba31e3ca4fca5 Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 10 Aug 2022 20:04:30 +0200 Subject: [PATCH 235/339] Don't surface partitions in service search sources (#14078) --- .../consul/service/search-bar/index.hbs | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs b/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs index f74926e25c..a73d0b6cad 100644 --- a/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs @@ -139,27 +139,8 @@ as |key value|}} - {{#let components.Optgroup components.Option as |Optgroup Option|}} -{{#let - (reject-by 'Partition' @partition @partitions) -as |nonDefaultPartitions|}} -{{#if (gt nonDefaultPartitions.length 0)}} - - {{#each @partitions as |partition|}} - - {{/each}} - -{{/if}} -{{/let}} - + {{#let components.Option as |Option|}} {{#if (gt @sources.length 0)}} - {{#each @sources as |source|}} - {{/if}} {{/let}} From 7b16b5e9f1db6a77394e878f8b9241b81f99b94f Mon Sep 17 00:00:00 2001 From: Michael Klein Date: Wed, 10 Aug 2022 20:07:59 +0200 Subject: [PATCH 236/339] ui: Improve display peer info in service list (#14111) * Include nspace when surfacing peer in bucket-list Whenever we display a peer and we are not on OSS we will surface the namespace as well. The rest of the ui logic of the bucket list has not changed. * Display bucket-list after instance-count service-list --- .../app/components/consul/bucket/list/index.js | 14 ++++++-------- .../app/components/consul/service/list/index.hbs | 10 +++++----- .../components/consul/bucket/list-test.js | 14 +++++++------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ui/packages/consul-ui/app/components/consul/bucket/list/index.js b/ui/packages/consul-ui/app/components/consul/bucket/list/index.js index c874ac91be..c1aa8afe92 100644 --- a/ui/packages/consul-ui/app/components/consul/bucket/list/index.js +++ b/ui/packages/consul-ui/app/components/consul/bucket/list/index.js @@ -58,7 +58,7 @@ export default class ConsulBucketList extends Component { get namespacePart() { const { item, nspace } = this.args; - const { abilities, partitionPart } = this; + const { abilities, partitionPart, peerPart } = this; const nspaceItem = { type: 'nspace', @@ -71,15 +71,13 @@ export default class ConsulBucketList extends Component { return [nspaceItem]; } + if (peerPart.length && abilities.can('use nspaces')) { + return [nspaceItem]; + } + if (nspace && abilities.can('use nspaces')) { if (item.Namespace !== nspace) { - return [ - { - type: 'nspace', - label: 'Namespace', - item: item.Namespace, - }, - ]; + return [nspaceItem]; } } diff --git a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs index 551cf027cf..a97184b984 100644 --- a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs @@ -56,6 +56,11 @@ {{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'instance' without-count=true}} {{/if}} + {{#if (eq item.Kind 'terminating-gateway')}} {{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}} @@ -87,11 +92,6 @@ {{/if}} {{/if}} - \ No newline at end of file diff --git a/ui/packages/consul-ui/tests/integration/components/consul/bucket/list-test.js b/ui/packages/consul-ui/tests/integration/components/consul/bucket/list-test.js index 15d6b81ce8..063f2b086d 100644 --- a/ui/packages/consul-ui/tests/integration/components/consul/bucket/list-test.js +++ b/ui/packages/consul-ui/tests/integration/components/consul/bucket/list-test.js @@ -224,31 +224,31 @@ module('Integration | Component | consul bucket list', function(hooks) { assert.dom('[data-test-bucket-item="partition"]').doesNotExist('partition is not displayed'); }); - test('it displays a peer and no nspace and no service when item.namespace and nspace match', async function(assert) { + test('it displays a peer and nspace when item.namespace and nspace match', async function(assert) { const PEER_NAME = 'Tomster'; const NAMESPACE_NAME = 'Mascot'; - const SERVICE_NAME = 'Ember.js'; this.set('peerName', PEER_NAME); this.set('namespace', NAMESPACE_NAME); - this.set('service', SERVICE_NAME); await render(hbs` `); assert.dom('[data-test-bucket-item="peer"]').hasText(PEER_NAME, 'Peer is displayed'); - assert.dom('[data-test-bucket-item="nspace"]').doesNotExist('namespace is not displayed'); - assert.dom('[data-test-bucket-item="service"]').doesNotExist('service is not displayed'); + assert + .dom('[data-test-bucket-item="nspace"]') + .hasText( + NAMESPACE_NAME, + 'namespace is displayed when peer is displayed and we are not on OSS (i.e. cannot use nspaces)' + ); assert.dom('[data-test-bucket-item="partition"]').doesNotExist('partition is not displayed'); }); }); From 15bd40ffc252e65be9d215d6e04901749990af68 Mon Sep 17 00:00:00 2001 From: Ashwin Venkatesh Date: Wed, 10 Aug 2022 14:25:12 -0400 Subject: [PATCH 237/339] Update website/content/docs/connect/cluster-peering/k8s.mdx Co-authored-by: Tu Nguyen --- .../docs/connect/cluster-peering/k8s.mdx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index a3716d3d71..b23bf74b16 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -238,12 +238,31 @@ To confirm that you deleted your peering connection, in `cluster-01`, query the $ curl "localhost:8500/v1/health/connect/backend?peer=cluster-02" ``` -## Recreate/Reset a peering connection +## Recreate or reset a peering connection -To recreate or reset the peering connection, a new peering token needs to be generated on the cluster where the `PeeringAcceptor` was created, which in this case is `cluster-01`. +To recreate or reset the peering connection, you need to generate a new peering token on the cluster where you created the `PeeringAcceptor` (in this example, `cluster-01`). -This can be performed by creating/updating the annotation `consul.hashicorp.com/peering-version` on the `PeeringAcceptor`. If the annotation already exists, update its value to a version that is higher. +1. You can do this by creating or updating the annotation `consul.hashicorp.com/peering-version` on the `PeeringAcceptor`. If the annotation already exists, update its value to a version that is higher. -Once the above is done, repeat the steps in the peering process from saving your peering token so that you can export it to the other cluster. This will re-establish peering with the updated token. + --> **NOTE:** A new peering token is only generated upon manually setting and updating the value of the annotation `consul.hashicorp.com/peering-version`. Creating a new token will cause the previous token to expire. + ```yaml + apiVersion: consul.hashicorp.com/v1alpha1 + kind: PeeringAcceptor + metadata: + name: cluster-02 + annotations: + consul.hashicorp.com/peering-version: 1 ## The peering version you want to set. + spec: + peer: + secret: + name: "peering-token" + key: "data" + backend: "kubernetes" + ``` + + + +1. Once you have done this, repeat the steps in the peering process. This includes saving your peering token so that you can export it to the other cluster. This will re-establish peering with the updated token. + +~> **Note:** A new peering token is only generated upon manually setting and updating the value of the annotation `consul.hashicorp.com/peering-version`. Creating a new token will cause the previous token to expire. From 7bec4050704f42bef3c1e355a79ff915527ec156 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Wed, 10 Aug 2022 11:57:09 -0700 Subject: [PATCH 238/339] docs: Update supported Envoy versions (#14130) --- website/content/docs/connect/proxies/envoy.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/proxies/envoy.mdx b/website/content/docs/connect/proxies/envoy.mdx index ee0b1b1652..526d642bc8 100644 --- a/website/content/docs/connect/proxies/envoy.mdx +++ b/website/content/docs/connect/proxies/envoy.mdx @@ -36,9 +36,9 @@ Consul supports **four major Envoy releases** at the beginning of each major Con | Consul Version | Compatible Envoy Versions | | ------------------- | -----------------------------------------------------------------------------------| +| 1.13.x | 1.23.0, 1.22.2, 1.21.4, 1.20.6 | | 1.12.x | 1.22.2, 1.21.3, 1.20.4, 1.19.5 | | 1.11.x | 1.20.2, 1.19.3, 1.18.6, 1.17.41 | -| 1.10.x | 1.18.6, 1.17.41, 1.16.51 , 1.15.51 | 1. Envoy 1.20.1 and earlier are vulnerable to [CVE-2022-21654](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21654) and [CVE-2022-21655](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21655). Both CVEs were patched in Envoy versions 1.18.6, 1.19.3, and 1.20.2. Envoy 1.16.x and older releases are no longer supported (see [HCSEC-2022-07](https://discuss.hashicorp.com/t/hcsec-2022-07-consul-s-connect-service-mesh-affected-by-recent-envoy-security-releases/36332)). Consul 1.9.x clusters should be upgraded to 1.10.x and Envoy upgraded to the latest supported Envoy version for that release, 1.18.6. From 641daa184172665aa71bd01089c229ba5f98c97b Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Wed, 10 Aug 2022 12:21:21 -0700 Subject: [PATCH 239/339] Sync changes from 1.13.0 release (#14104) --- CHANGELOG.md | 115 +++++++++++++++++++++++---------------------- version/version.go | 2 +- 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d77193026b..0c47698349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,63 @@ +## 1.13.0 (August 9, 2022) + +BREAKING CHANGES: + +* config-entry: Exporting a specific service name across all namespace is invalid. +* connect: Removes support for Envoy 1.19 [[GH-13807](https://github.com/hashicorp/consul/issues/13807)] +* telemetry: config flag `telemetry { disable_compat_1.9 = (true|false) }` has been removed. Before upgrading you should remove this flag from your config if the flag is being used. [[GH-13532](https://github.com/hashicorp/consul/issues/13532)] + +FEATURES: + +* **Cluster Peering (Beta)** This version adds a new model to federate Consul clusters for both service mesh and traditional service discovery. Cluster peering allows for service interconnectivity with looser coupling than the existing WAN federation. For more information refer to the [cluster peering](https://www.consul.io/docs/connect/cluster-peering) documentation. +* **Transparent proxying through terminating gateways** This version adds egress traffic control to destinations outside of Consul's catalog, such as APIs on the public internet. Transparent proxies can dial [destinations defined in service-defaults](https://www.consul.io/docs/connect/config-entries/service-defaults#destination) and have the traffic routed through terminating gateways. For more information refer to the [terminating gateway](https://www.consul.io/docs/connect/gateways/terminating-gateway#terminating-gateway-configuration) documentation. +* acl: It is now possible to login and logout using the gRPC API [[GH-12935](https://github.com/hashicorp/consul/issues/12935)] +* agent: Added information about build date alongside other version information for Consul. Extended /agent/self endpoint and `consul version` commands +to report this. Agent also reports build date in log on startup. [[GH-13357](https://github.com/hashicorp/consul/issues/13357)] +* ca: Leaf certificates can now be obtained via the gRPC API: `Sign` [[GH-12787](https://github.com/hashicorp/consul/issues/12787)] +* checks: add UDP health checks.. [[GH-12722](https://github.com/hashicorp/consul/issues/12722)] +* cli: A new flag for config delete to delete a config entry in a +valid config file, e.g., config delete -filename intention-allow.hcl [[GH-13677](https://github.com/hashicorp/consul/issues/13677)] +* connect: Adds a new `destination` field to the `service-default` config entry that allows routing egress traffic +through a terminating gateway in transparent proxy mode without modifying the catalog. [[GH-13613](https://github.com/hashicorp/consul/issues/13613)] +* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-12825](https://github.com/hashicorp/consul/issues/12825)] +* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-1717](https://github.com/hashicorp/consul/issues/1717)] +* grpc: New gRPC service and endpoint to return the list of supported consul dataplane features [[GH-12695](https://github.com/hashicorp/consul/issues/12695)] +* server: broadcast the public grpc port using lan serf and update the consul service in the catalog with the same data [[GH-13687](https://github.com/hashicorp/consul/issues/13687)] +* streaming: Added topic that can be used to consume updates about the list of services in a datacenter [[GH-13722](https://github.com/hashicorp/consul/issues/13722)] +* streaming: Added topics for `ingress-gateway`, `mesh`, `service-intentions` and `service-resolver` config entry events. [[GH-13658](https://github.com/hashicorp/consul/issues/13658)] + +IMPROVEMENTS: + +* api: `merge-central-config` query parameter support added to `/catalog/node-services/:node-name` API, to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13450](https://github.com/hashicorp/consul/issues/13450)] +* api: `merge-central-config` query parameter support added to `/catalog/node-services/:node-name` API, to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-2046](https://github.com/hashicorp/consul/issues/2046)] +* api: `merge-central-config` query parameter support added to some catalog and health endpoints to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13001](https://github.com/hashicorp/consul/issues/13001)] +* api: add the ability to specify a path prefix for when consul is behind a reverse proxy or API gateway [[GH-12914](https://github.com/hashicorp/consul/issues/12914)] +* catalog: Add per-node indexes to reduce watchset firing for unrelated nodes and services. [[GH-12399](https://github.com/hashicorp/consul/issues/12399)] +* connect: add validation to ensure connect native services have a port or socketpath specified on catalog registration. +This was the only missing piece to ensure all mesh services are validated for a port (or socketpath) specification on catalog registration. [[GH-12881](https://github.com/hashicorp/consul/issues/12881)] +* ui: Add new CopyableCode component and use it in certain pre-existing areas [[GH-13686](https://github.com/hashicorp/consul/issues/13686)] +* acl: Clarify node/service identities must be lowercase [[GH-12807](https://github.com/hashicorp/consul/issues/12807)] +* command: Add support for enabling TLS in the Envoy Prometheus endpoint via the `consul connect envoy` command. +Adds the `-prometheus-ca-file`, `-prometheus-ca-path`, `-prometheus-cert-file` and `-prometheus-key-file` flags. [[GH-13481](https://github.com/hashicorp/consul/issues/13481)] +* connect: Add Envoy 1.23.0 to support matrix [[GH-13807](https://github.com/hashicorp/consul/issues/13807)] +* connect: Added a `max_inbound_connections` setting to service-defaults for limiting the number of concurrent inbound connections to each service instance. [[GH-13143](https://github.com/hashicorp/consul/issues/13143)] +* grpc: Add a new ServerDiscovery.WatchServers gRPC endpoint for being notified when the set of ready servers has changed. [[GH-12819](https://github.com/hashicorp/consul/issues/12819)] +* telemetry: Added `consul.raft.thread.main.saturation` and `consul.raft.thread.fsm.saturation` metrics to measure approximate saturation of the Raft goroutines [[GH-12865](https://github.com/hashicorp/consul/issues/12865)] +* ui: removed external dependencies for serving UI assets in favor of Go's native embed capabilities [[GH-10996](https://github.com/hashicorp/consul/issues/10996)] +* ui: upgrade ember-composable-helpers to v5.x [[GH-13394](https://github.com/hashicorp/consul/issues/13394)] + +BUG FIXES: + +* acl: Fixed a bug where the ACL down policy wasn't being applied on remote errors from the primary datacenter. [[GH-12885](https://github.com/hashicorp/consul/issues/12885)] +* cli: when `acl token read` is used with the `-self` and `-expanded` flags, return an error instead of panicking [[GH-13787](https://github.com/hashicorp/consul/issues/13787)] +* connect: Fixed a goroutine/memory leak that would occur when using the ingress gateway. [[GH-13847](https://github.com/hashicorp/consul/issues/13847)] +* connect: Ingress gateways with a wildcard service entry should no longer pick up non-connect services as upstreams. +connect: Terminating gateways with a wildcard service entry should no longer pick up connect services as upstreams. [[GH-13958](https://github.com/hashicorp/consul/issues/13958)] +* proxycfg: Fixed a minor bug that would cause configuring a terminating gateway to watch too many service resolvers and waste resources doing filtering. [[GH-13012](https://github.com/hashicorp/consul/issues/13012)] +* raft: upgrade to v1.3.8 which fixes a bug where non cluster member can still be able to participate in an election. [[GH-12844](https://github.com/hashicorp/consul/issues/12844)] +* serf: upgrade serf to v0.9.8 which fixes a bug that crashes Consul when serf keyrings are listed [[GH-13062](https://github.com/hashicorp/consul/issues/13062)] +* ui: Fixes an issue where client side validation errors were not showing in certain areas [[GH-14021](https://github.com/hashicorp/consul/issues/14021)] + ## 1.12.3 (July 13, 2022) IMPROVEMENTS: @@ -36,61 +96,6 @@ BUG FIXES: * agent: Fixed a bug in HTTP handlers where URLs were being decoded twice [[GH-13264](https://github.com/hashicorp/consul/issues/13264)] * fix a bug that caused an error when creating `grpc` or `http2` ingress gateway listeners with multiple services [[GH-13127](https://github.com/hashicorp/consul/issues/13127)] -## 1.13.0-alpha2 (June 21, 2022) - -IMPROVEMENTS: - -* api: `merge-central-config` query parameter support added to `/catalog/node-services/:node-name` API, to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13450](https://github.com/hashicorp/consul/issues/13450)] -* connect: Update Envoy support matrix to latest patch releases (1.22.2, 1.21.3, 1.20.4, 1.19.5) [[GH-13431](https://github.com/hashicorp/consul/issues/13431)] - -BUG FIXES: - -* ui: Fix incorrect text on certain page empty states [[GH-13409](https://github.com/hashicorp/consul/issues/13409)] - -## 1.13.0-alpha1 (June 15, 2022) - -BREAKING CHANGES: - -* config-entry: Exporting a specific service name across all namespace is invalid. - -FEATURES: - -* acl: It is now possible to login and logout using the gRPC API [[GH-12935](https://github.com/hashicorp/consul/issues/12935)] -* agent: Added information about build date alongside other version information for Consul. Extended /agent/self endpoint and `consul version` commands -to report this. Agent also reports build date in log on startup. [[GH-13357](https://github.com/hashicorp/consul/issues/13357)] -* ca: Leaf certificates can now be obtained via the gRPC API: `Sign` [[GH-12787](https://github.com/hashicorp/consul/issues/12787)] -* checks: add UDP health checks.. [[GH-12722](https://github.com/hashicorp/consul/issues/12722)] -* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-12825](https://github.com/hashicorp/consul/issues/12825)] -* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-1717](https://github.com/hashicorp/consul/issues/1717)] -* grpc: New gRPC service and endpoint to return the list of supported consul dataplane features [[GH-12695](https://github.com/hashicorp/consul/issues/12695)] - -IMPROVEMENTS: - -* api: `merge-central-config` query parameter support added to some catalog and health endpoints to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13001](https://github.com/hashicorp/consul/issues/13001)] -* api: add the ability to specify a path prefix for when consul is behind a reverse proxy or API gateway [[GH-12914](https://github.com/hashicorp/consul/issues/12914)] -* connect: add validation to ensure connect native services have a port or socketpath specified on catalog registration. -This was the only missing piece to ensure all mesh services are validated for a port (or socketpath) specification on catalog registration. [[GH-12881](https://github.com/hashicorp/consul/issues/12881)] -* Support Vault namespaces in Connect CA by adding RootPKINamespace and -IntermediatePKINamespace fields to the config. [[GH-12904](https://github.com/hashicorp/consul/issues/12904)] -* acl: Clarify node/service identities must be lowercase [[GH-12807](https://github.com/hashicorp/consul/issues/12807)] -* connect: Added a `max_inbound_connections` setting to service-defaults for limiting the number of concurrent inbound connections to each service instance. [[GH-13143](https://github.com/hashicorp/consul/issues/13143)] -* dns: Added support for specifying admin partition in node lookups. [[GH-13421](https://github.com/hashicorp/consul/issues/13421)] -* grpc: Add a new ServerDiscovery.WatchServers gRPC endpoint for being notified when the set of ready servers has changed. [[GH-12819](https://github.com/hashicorp/consul/issues/12819)] -* telemetry: Added `consul.raft.thread.main.saturation` and `consul.raft.thread.fsm.saturation` metrics to measure approximate saturation of the Raft goroutines [[GH-12865](https://github.com/hashicorp/consul/issues/12865)] -* telemetry: Added a `consul.server.isLeader` metric to track if a server is a leader or not. [[GH-13304](https://github.com/hashicorp/consul/issues/13304)] -* ui: removed external dependencies for serving UI assets in favor of Go's native embed capabilities [[GH-10996](https://github.com/hashicorp/consul/issues/10996)] -* ui: upgrade ember-composable-helpers to v5.x [[GH-13394](https://github.com/hashicorp/consul/issues/13394)] - -BUG FIXES: - -* acl: Fixed a bug where the ACL down policy wasn't being applied on remote errors from the primary datacenter. [[GH-12885](https://github.com/hashicorp/consul/issues/12885)] -* agent: Fixed a bug in HTTP handlers where URLs were being decoded twice [[GH-13256](https://github.com/hashicorp/consul/issues/13256)] -* deps: Update go-grpc/grpc, resolving connection memory leak [[GH-13051](https://github.com/hashicorp/consul/issues/13051)] -* fix a bug that caused an error when creating `grpc` or `http2` ingress gateway listeners with multiple services [[GH-13127](https://github.com/hashicorp/consul/issues/13127)] -* proxycfg: Fixed a minor bug that would cause configuring a terminating gateway to watch too many service resolvers and waste resources doing filtering. [[GH-13012](https://github.com/hashicorp/consul/issues/13012)] -* raft: upgrade to v1.3.8 which fixes a bug where non cluster member can still be able to participate in an election. [[GH-12844](https://github.com/hashicorp/consul/issues/12844)] -* serf: upgrade serf to v0.9.8 which fixes a bug that crashes Consul when serf keyrings are listed [[GH-13062](https://github.com/hashicorp/consul/issues/13062)] - ## 1.12.2 (June 3, 2022) BUG FIXES: diff --git a/version/version.go b/version/version.go index edcdca85b3..8930a432a8 100644 --- a/version/version.go +++ b/version/version.go @@ -14,7 +14,7 @@ var ( // // Version must conform to the format expected by github.com/hashicorp/go-version // for tests to work. - Version = "1.13.0" + Version = "1.14.0" // https://semver.org/#spec-item-10 VersionMetadata = "" From 56b12ad3a3244ad033221cdf6f62480c4d51314a Mon Sep 17 00:00:00 2001 From: "A.J. Sanon" <47250909+sanon-dev@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:17:56 -0400 Subject: [PATCH 240/339] Add Consul ECS v0.5 release notes (#14010) --- .../docs/release-notes/consul-ecs/v0_4_x.mdx | 6 ++-- .../docs/release-notes/consul-ecs/v0_5_x.mdx | 30 +++++++++++++++++++ website/data/docs-nav-data.json | 4 +++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 website/content/docs/release-notes/consul-ecs/v0_5_x.mdx diff --git a/website/content/docs/release-notes/consul-ecs/v0_4_x.mdx b/website/content/docs/release-notes/consul-ecs/v0_4_x.mdx index 18e6028a94..5ada947226 100644 --- a/website/content/docs/release-notes/consul-ecs/v0_4_x.mdx +++ b/website/content/docs/release-notes/consul-ecs/v0_4_x.mdx @@ -5,7 +5,7 @@ description: >- Consul ECS release notes for version 0.4.x --- -# Consul ECS 0.4.0 +# Consul ECS 0.4.x ## Release Highlights @@ -23,6 +23,6 @@ The changelogs for this major release version and any maintenance versions are l -> **Note**: These links will take you to the changelogs on the GitHub website. -- [0.4.0](https://github.com/hashicorp/consul-ecs/releases/tag/v0.4.0) - - [0.4.1](https://github.com/hashicorp/consul-ecs/releases/tag/v0.4.1) + +- [0.4.0](https://github.com/hashicorp/consul-ecs/releases/tag/v0.4.0) diff --git a/website/content/docs/release-notes/consul-ecs/v0_5_x.mdx b/website/content/docs/release-notes/consul-ecs/v0_5_x.mdx new file mode 100644 index 0000000000..54b29b3b30 --- /dev/null +++ b/website/content/docs/release-notes/consul-ecs/v0_5_x.mdx @@ -0,0 +1,30 @@ +--- +layout: docs +page_title: 0.5.x +description: >- + Consul ECS release notes for version 0.5.x +--- + +# Consul ECS 0.5.x + +## Release Highlights + +- **Audit Logging (Enterprise) :** Consul on ECS now captures authentication events and processes them with the HTTP API. Audit logging provides insight into access and usage patterns. Refer to [Audit Logging](/docs/ecs/enterprise#audit-logging) for usage information. + +- **AWS IAM Auth Method :** This feature provides support for Consul's AWS IAM auth method. This allows AWS IAM roles and users to authenticate with Consul to obtain ACL tokens. Refer to [ECS Configuration Reference](/docs/ecs/configuration-reference#consullogin) for configuration information. + +- **Mesh Gateways :** This feature introduces support for running mesh gateways as ECS tasks. Mesh gateways enable service mesh communication across datacenter and admin partition boundaries. Refer to [ECS Installation with Terraform](/docs/ecs/terraform/install#configure-the-gateway-task-module) for usage information. + +## Supported Software Versions + +- Consul: 1.12.x + +## Changelogs + +The changelogs for this major release version and any maintenance versions are listed below. + +-> **Note**: These links will take you to the changelogs on the GitHub website. + +- [0.5.1](https://github.com/hashicorp/consul-ecs/releases/tag/v0.5.1) + +- [0.5.0](https://github.com/hashicorp/consul-ecs/releases/tag/v0.5.0) diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 71ee064f0e..b7c2117415 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -1278,6 +1278,10 @@ { "title": "Consul ECS", "routes": [ + { + "title": "v0.5.x", + "path": "release-notes/consul-ecs/v0_5_x" + }, { "title": "v0.4.x", "path": "release-notes/consul-ecs/v0_4_x" From 156fbcbc0c504e78e89dccd254c0c4740f81a557 Mon Sep 17 00:00:00 2001 From: Ashwin Venkatesh Date: Wed, 10 Aug 2022 16:53:45 -0400 Subject: [PATCH 241/339] Update website/content/docs/connect/cluster-peering/k8s.mdx Co-authored-by: Tu Nguyen --- website/content/docs/connect/cluster-peering/k8s.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index b23bf74b16..26d86cf312 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -244,7 +244,7 @@ To recreate or reset the peering connection, you need to generate a new peering 1. You can do this by creating or updating the annotation `consul.hashicorp.com/peering-version` on the `PeeringAcceptor`. If the annotation already exists, update its value to a version that is higher. - + ```yaml apiVersion: consul.hashicorp.com/v1alpha1 From 3c4fa9b4684dc62be1faa9da6a3a3c6cef3d0fb9 Mon Sep 17 00:00:00 2001 From: Daniel Kimsey Date: Wed, 10 Aug 2022 16:52:32 -0500 Subject: [PATCH 242/339] Add support for filtering the 'List Services' API 1. Create a bexpr filter for performing the filtering 2. Change the state store functions to return the raw (not aggregated) list of ServiceNodes. 3. Move the aggregate service tags by name logic out of the state store functions into a new function called from the RPC endpoint 4. Perform the filtering in the endpoint before aggregation. --- .changelog/11742.txt | 3 ++ agent/consul/catalog_endpoint.go | 42 ++++++++++++++++++- agent/consul/catalog_endpoint_test.go | 39 ++++++++++++++++++ agent/consul/state/catalog.go | 55 ++++--------------------- agent/consul/state/catalog_test.go | 56 +++++++++++++------------- agent/consul/state/state_store_test.go | 11 ++--- website/content/api-docs/catalog.mdx | 53 +++++++++++++++++++++++- 7 files changed, 177 insertions(+), 82 deletions(-) create mode 100644 .changelog/11742.txt diff --git a/.changelog/11742.txt b/.changelog/11742.txt new file mode 100644 index 0000000000..6c6d4c2498 --- /dev/null +++ b/.changelog/11742.txt @@ -0,0 +1,3 @@ +```release-note:improvement +api: Add filtering support to Catalog's List Services (v1/catalog/services) +``` diff --git a/agent/consul/catalog_endpoint.go b/agent/consul/catalog_endpoint.go index 111ee7b2ba..696ae314a7 100644 --- a/agent/consul/catalog_endpoint.go +++ b/agent/consul/catalog_endpoint.go @@ -565,6 +565,11 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I return err } + filter, err := bexpr.CreateFilter(args.Filter, nil, []*structs.ServiceNode{}) + if err != nil { + return err + } + // Set reply enterprise metadata after resolving and validating the token so // that we can properly infer metadata from the token. reply.EnterpriseMeta = args.EnterpriseMeta @@ -574,10 +579,11 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { var err error + var serviceNodes structs.ServiceNodes if len(args.NodeMetaFilters) > 0 { - reply.Index, reply.Services, err = state.ServicesByNodeMeta(ws, args.NodeMetaFilters, &args.EnterpriseMeta, args.PeerName) + reply.Index, serviceNodes, err = state.ServicesByNodeMeta(ws, args.NodeMetaFilters, &args.EnterpriseMeta, args.PeerName) } else { - reply.Index, reply.Services, err = state.Services(ws, &args.EnterpriseMeta, args.PeerName) + reply.Index, serviceNodes, err = state.Services(ws, &args.EnterpriseMeta, args.PeerName) } if err != nil { return err @@ -588,11 +594,43 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I return nil } + raw, err := filter.Execute(serviceNodes) + if err != nil { + return err + } + + reply.Services = servicesTagsByName(raw.(structs.ServiceNodes)) + c.srv.filterACLWithAuthorizer(authz, reply) + return nil }) } +func servicesTagsByName(services []*structs.ServiceNode) structs.Services { + unique := make(map[string]map[string]struct{}) + for _, svc := range services { + tags, ok := unique[svc.ServiceName] + if !ok { + unique[svc.ServiceName] = make(map[string]struct{}) + tags = unique[svc.ServiceName] + } + for _, tag := range svc.ServiceTags { + tags[tag] = struct{}{} + } + } + + // Generate the output structure. + var results = make(structs.Services) + for service, tags := range unique { + results[service] = make([]string, 0, len(tags)) + for tag := range tags { + results[service] = append(results[service], tag) + } + } + return results +} + // ServiceList is used to query the services in a DC. // Returns services as a list of ServiceNames. func (c *Catalog) ServiceList(args *structs.DCSpecificRequest, reply *structs.IndexedServiceList) error { diff --git a/agent/consul/catalog_endpoint_test.go b/agent/consul/catalog_endpoint_test.go index ca00efaea2..daa22c90c1 100644 --- a/agent/consul/catalog_endpoint_test.go +++ b/agent/consul/catalog_endpoint_test.go @@ -1523,6 +1523,45 @@ func TestCatalog_ListServices_NodeMetaFilter(t *testing.T) { } } +func TestCatalog_ListServices_Filter(t *testing.T) { + t.Parallel() + _, s1 := testServer(t) + codec := rpcClient(t, s1) + + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + + // prep the cluster with some data we can use in our filters + registerTestCatalogEntries(t, codec) + + // Run the tests against the test server + + t.Run("ListServices", func(t *testing.T) { + args := structs.DCSpecificRequest{ + Datacenter: "dc1", + } + + args.Filter = "ServiceName == redis" + out := new(structs.IndexedServices) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + require.Contains(t, out.Services, "redis") + require.ElementsMatch(t, []string{"v1", "v2"}, out.Services["redis"]) + + args.Filter = "NodeMeta.os == NoSuchOS" + out = new(structs.IndexedServices) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + require.Len(t, out.Services, 0) + + args.Filter = "NodeMeta.NoSuchMetadata == linux" + out = new(structs.IndexedServices) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + require.Len(t, out.Services, 0) + + args.Filter = "InvalidField == linux" + out = new(structs.IndexedServices) + require.Error(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + }) +} + func TestCatalog_ListServices_Blocking(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 258519d5ba..879c59f747 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -1134,7 +1134,7 @@ func terminatingGatewayVirtualIPsSupported(tx ReadTxn, ws memdb.WatchSet) (bool, } // Services returns all services along with a list of associated tags. -func (s *Store) Services(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.Services, error) { +func (s *Store) Services(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerName string) (uint64, []*structs.ServiceNode, error) { tx := s.db.Txn(false) defer tx.Abort() @@ -1148,30 +1148,11 @@ func (s *Store) Services(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerNam } ws.Add(services.WatchCh()) - // Rip through the services and enumerate them and their unique set of - // tags. - unique := make(map[string]map[string]struct{}) + var result []*structs.ServiceNode for service := services.Next(); service != nil; service = services.Next() { - svc := service.(*structs.ServiceNode) - tags, ok := unique[svc.ServiceName] - if !ok { - unique[svc.ServiceName] = make(map[string]struct{}) - tags = unique[svc.ServiceName] - } - for _, tag := range svc.ServiceTags { - tags[tag] = struct{}{} - } + result = append(result, service.(*structs.ServiceNode)) } - - // Generate the output structure. - var results = make(structs.Services) - for service, tags := range unique { - results[service] = make([]string, 0, len(tags)) - for tag := range tags { - results[service] = append(results[service], tag) - } - } - return idx, results, nil + return idx, result, nil } func (s *Store) ServiceList(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.ServiceList, error) { @@ -1212,7 +1193,7 @@ func serviceListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, } // ServicesByNodeMeta returns all services, filtered by the given node metadata. -func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.Services, error) { +func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, entMeta *acl.EnterpriseMeta, peerName string) (uint64, []*structs.ServiceNode, error) { tx := s.db.Txn(false) defer tx.Abort() @@ -1259,8 +1240,7 @@ func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, } allServicesCh := allServices.WatchCh() - // Populate the services map - unique := make(map[string]map[string]struct{}) + var result structs.ServiceNodes for node := nodes.Next(); node != nil; node = nodes.Next() { n := node.(*structs.Node) if len(filters) > 1 && !structs.SatisfiesMetaFilters(n.Meta, filters) { @@ -1274,30 +1254,11 @@ func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, } ws.AddWithLimit(watchLimit, services.WatchCh(), allServicesCh) - // Rip through the services and enumerate them and their unique set of - // tags. for service := services.Next(); service != nil; service = services.Next() { - svc := service.(*structs.ServiceNode) - tags, ok := unique[svc.ServiceName] - if !ok { - unique[svc.ServiceName] = make(map[string]struct{}) - tags = unique[svc.ServiceName] - } - for _, tag := range svc.ServiceTags { - tags[tag] = struct{}{} - } + result = append(result, service.(*structs.ServiceNode)) } } - - // Generate the output structure. - var results = make(structs.Services) - for service, tags := range unique { - results[service] = make([]string, 0, len(tags)) - for tag := range tags { - results[service] = append(results[service], tag) - } - } - return idx, results, nil + return idx, result, nil } // maxIndexForService return the maximum Raft Index for a service diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index 10e7af6dba..ca2bded03b 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/consul/acl" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/assert" @@ -2105,7 +2107,7 @@ func TestStateStore_Services(t *testing.T) { if err := s.EnsureService(2, "node1", ns1); err != nil { t.Fatalf("err: %s", err) } - testRegisterService(t, s, 3, "node1", "dogs") + ns1Dogs := testRegisterService(t, s, 3, "node1", "dogs") testRegisterNode(t, s, 4, "node2") ns2 := &structs.NodeService{ ID: "service3", @@ -2131,19 +2133,13 @@ func TestStateStore_Services(t *testing.T) { t.Fatalf("bad index: %d", idx) } - // Verify the result. We sort the lists since the order is - // non-deterministic (it's built using a map internally). - expected := structs.Services{ - "redis": []string{"prod", "primary", "replica"}, - "dogs": []string{}, - } - sort.Strings(expected["redis"]) - for _, tags := range services { - sort.Strings(tags) - } - if !reflect.DeepEqual(expected, services) { - t.Fatalf("bad: %#v", services) + // Verify the result. + expected := []*structs.ServiceNode{ + ns1Dogs.ToServiceNode("node1"), + ns1.ToServiceNode("node1"), + ns2.ToServiceNode("node2"), } + assertDeepEqual(t, services, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) // Deleting a node with a service should fire the watch. if err := s.DeleteNode(6, "node1", nil, ""); err != nil { @@ -2206,11 +2202,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{ - "redis": []string{"primary", "prod"}, + expected := []*structs.ServiceNode{ + ns1.ToServiceNode("node0"), } - sort.Strings(res["redis"]) - require.Equal(t, expected, res) + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Get all services using the common meta value", func(t *testing.T) { @@ -2218,11 +2213,12 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{ - "redis": []string{"primary", "prod", "replica"}, + require.Len(t, res, 2) + expected := []*structs.ServiceNode{ + ns1.ToServiceNode("node0"), + ns2.ToServiceNode("node1"), } - sort.Strings(res["redis"]) - require.Equal(t, expected, res) + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Get an empty list for an invalid meta value", func(t *testing.T) { @@ -2230,8 +2226,8 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{} - require.Equal(t, expected, res) + var expected []*structs.ServiceNode + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Get the first node's service instance using multiple meta filters", func(t *testing.T) { @@ -2239,11 +2235,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{ - "redis": []string{"primary", "prod"}, + expected := []*structs.ServiceNode{ + ns1.ToServiceNode("node0"), } - sort.Strings(res["redis"]) - require.Equal(t, expected, res) + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Registering some unrelated node + service should not fire the watch.", func(t *testing.T) { @@ -8807,3 +8802,10 @@ func setVirtualIPFlags(t *testing.T, s *Store) { Value: "true", })) } + +func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) { + t.Helper() + if diff := cmp.Diff(x, y, opts...); diff != "" { + t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff) + } +} diff --git a/agent/consul/state/state_store_test.go b/agent/consul/state/state_store_test.go index c8460ca821..88e5418c8d 100644 --- a/agent/consul/state/state_store_test.go +++ b/agent/consul/state/state_store_test.go @@ -146,13 +146,13 @@ func testRegisterServiceOpts(t *testing.T, s *Store, idx uint64, nodeID, service // testRegisterServiceWithChange registers a service and allow ensuring the consul index is updated // even if service already exists if using `modifyAccordingIndex`. // This is done by setting the transaction ID in "version" meta so service will be updated if it already exists -func testRegisterServiceWithChange(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool) { - testRegisterServiceWithChangeOpts(t, s, idx, nodeID, serviceID, modifyAccordingIndex) +func testRegisterServiceWithChange(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool) *structs.NodeService { + return testRegisterServiceWithChangeOpts(t, s, idx, nodeID, serviceID, modifyAccordingIndex) } // testRegisterServiceWithChangeOpts is the same as testRegisterServiceWithChange with the addition of opts that can // modify the service prior to writing. -func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool, opts ...func(service *structs.NodeService)) { +func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool, opts ...func(service *structs.NodeService)) *structs.NodeService { meta := make(map[string]string) if modifyAccordingIndex { meta["version"] = fmt.Sprint(idx) @@ -183,14 +183,15 @@ func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeI result.ServiceID != serviceID { t.Fatalf("bad service: %#v", result) } + return svc } // testRegisterService register a service with given transaction idx // If the service already exists, transaction number might not be increased // Use `testRegisterServiceWithChange()` if you want perform a registration that // ensures the transaction is updated by setting idx in Meta of Service -func testRegisterService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) { - testRegisterServiceWithChange(t, s, idx, nodeID, serviceID, false) +func testRegisterService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) *structs.NodeService { + return testRegisterServiceWithChange(t, s, idx, nodeID, serviceID, false) } func testRegisterConnectService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) { diff --git a/website/content/api-docs/catalog.mdx b/website/content/api-docs/catalog.mdx index b259176850..86480ab79b 100644 --- a/website/content/api-docs/catalog.mdx +++ b/website/content/api-docs/catalog.mdx @@ -410,13 +410,64 @@ The corresponding CLI command is [`consul catalog services`](/commands/catalog/s - `dc` `(string: "")` - Specifies the datacenter to query. This will default to the datacenter of the agent being queried. -- `node-meta` `(string: "")` - Specifies a desired node metadata key/value pair +- `node-meta` `(string: "")` **Deprecated** - Use `filter` with the `NodeMeta` selector instead. + This parameter will be removed in a future version of Consul. + Specifies a desired node metadata key/value pair of the form `key:value`. This parameter can be specified multiple times, and filters the results to nodes with the specified key/value pairs. - `ns` `(string: "")` - Specifies the namespace of the services you lookup. You can also [specify the namespace through other methods](#methods-to-specify-namespace). +- `filter` `(string: "")` - Specifies the expression used to filter the + queries results prior to returning the data. + +### Filtering + +The filter will be executed against each Service mapping within the catalog. +The following selectors and filter operations are supported: + +| Selector | Supported Operations | +| ---------------------------------------------------- | -------------------------------------------------- | +| `Address` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `Datacenter` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ID` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `Node` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `NodeMeta.` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `NodeMeta` | Is Empty, Is Not Empty, In, Not In | +| `ServiceAddress` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceConnect.Native` | Equal, Not Equal | +| `ServiceEnableTagOverride` | Equal, Not Equal | +| `ServiceID` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceKind` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceMeta.` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceMeta` | Is Empty, Is Not Empty, In, Not In | +| `ServiceName` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServicePort` | Equal, Not Equal | +| `ServiceProxy.DestinationServiceID` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.DestinationServiceName` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.LocalServiceAddress` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.LocalServicePort` | Equal, Not Equal | +| `ServiceProxy.Mode` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.TransparentProxy.OutboundListenerPort` | Equal, Not Equal | +| `ServiceProxy.MeshGateway.Mode` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams.Datacenter` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams.DestinationName` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams.DestinationNamespace` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams.DestinationType` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams.LocalBindAddress` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams.LocalBindPort` | Equal, Not Equal | +| `ServiceProxy.Upstreams.MeshGateway.Mode` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceProxy.Upstreams` | Is Empty, Is Not Empty | +| `ServiceTaggedAddresses..Address` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `ServiceTaggedAddresses..Port` | Equal, Not Equal | +| `ServiceTaggedAddresses` | Is Empty, Is Not Empty, In, Not In | +| `ServiceTags` | In, Not In, Is Empty, Is Not Empty | +| `ServiceWeights.Passing` | Equal, Not Equal | +| `ServiceWeights.Warning` | Equal, Not Equal | +| `TaggedAddresses.` | Equal, Not Equal, In, Not In, Matches, Not Matches | +| `TaggedAddresses` | Is Empty, Is Not Empty, In, Not In | + ### Sample Request ```shell-session From 6ce3669b821f1e8f8e696c728cfd0547e6b68f3f Mon Sep 17 00:00:00 2001 From: boruszak Date: Wed, 10 Aug 2022 16:54:42 -0500 Subject: [PATCH 243/339] Blank commit --- .../mesh-gateway/service-to-service-traffic-partitions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx index dfd4780c6a..f3542c4d61 100644 --- a/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx +++ b/website/content/docs/connect/gateways/mesh-gateway/service-to-service-traffic-partitions.mdx @@ -3,7 +3,7 @@ layout: docs page_title: Service-to-service Traffic Across Partitions description: >- This topic describes how to configure mesh gateways to route a service's data to upstreams - in other partitions. It describes how to use Envoy and how you can integrate with your preferred gateway. + in other partitions. It describes how to use Envoy and how you can integrate with your preferred gateway. --- # Service-to-service Traffic Across Partitions From de731712028c6e83a3ac499ce5ac7241cb9f93ec Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Wed, 10 Aug 2022 11:53:25 -0400 Subject: [PATCH 244/339] Handle wrapped errors in isFailedPreconditionErr --- agent/consul/leader_peering.go | 9 +++++++++ agent/consul/leader_peering_test.go | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/agent/consul/leader_peering.go b/agent/consul/leader_peering.go index dd6185a194..bc5b669cdd 100644 --- a/agent/consul/leader_peering.go +++ b/agent/consul/leader_peering.go @@ -606,6 +606,15 @@ func isFailedPreconditionErr(err error) bool { if err == nil { return false } + + // Handle wrapped errors, since status.FromError does a naive assertion. + var statusErr interface { + GRPCStatus() *grpcstatus.Status + } + if errors.As(err, &statusErr) { + return statusErr.GRPCStatus().Code() == codes.FailedPrecondition + } + grpcErr, ok := grpcstatus.FromError(err) if !ok { return false diff --git a/agent/consul/leader_peering_test.go b/agent/consul/leader_peering_test.go index 48e48e14bb..46a74b6ad3 100644 --- a/agent/consul/leader_peering_test.go +++ b/agent/consul/leader_peering_test.go @@ -12,6 +12,7 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -1332,3 +1333,13 @@ func TestLeader_Peering_retryLoopBackoffPeering_cancelContext(t *testing.T) { fmt.Errorf("error 1"), }, allErrors) } + +func Test_isFailedPreconditionErr(t *testing.T) { + st := grpcstatus.New(codes.FailedPrecondition, "cannot establish a peering stream on a follower node") + err := st.Err() + assert.True(t, isFailedPreconditionErr(err)) + + // test that wrapped errors are checked correctly + werr := fmt.Errorf("wrapped: %w", err) + assert.True(t, isFailedPreconditionErr(werr)) +} From 1ef22360c3412c23ae0351885cc868f1674d2a64 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Wed, 10 Aug 2022 15:42:54 -0400 Subject: [PATCH 245/339] Register peerStreamServer internally to enable RPC forwarding --- agent/consul/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/agent/consul/server.go b/agent/consul/server.go index 10b9d48f07..1afa74c91d 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -816,6 +816,7 @@ func newGRPCHandlerFromConfig(deps Deps, config *Config, s *Server) connHandler // Note: these external gRPC services are also exposed on the internal server to // enable RPC forwarding. + s.peerStreamServer.Register(srv) s.externalACLServer.Register(srv) s.externalConnectCAServer.Register(srv) } From 3926009405a4514ca306079248f8a23264fa85ba Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Wed, 10 Aug 2022 18:31:55 -0400 Subject: [PATCH 246/339] Add test to verify forwarding --- agent/consul/peering_backend_test.go | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/agent/consul/peering_backend_test.go b/agent/consul/peering_backend_test.go index fc73ba53d0..7636dc48be 100644 --- a/agent/consul/peering_backend_test.go +++ b/agent/consul/peering_backend_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/consul/agent/pool" "github.com/hashicorp/consul/proto/pbpeering" + "github.com/hashicorp/consul/proto/pbpeerstream" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" ) @@ -76,3 +77,62 @@ func newServerDialer(serverAddr string) func(context.Context, string) (net.Conn, return conn, nil } } + +func TestPeerStreamService_ForwardToLeader(t *testing.T) { + t.Parallel() + + _, conf1 := testServerConfig(t) + server1, err := newServer(t, conf1) + require.NoError(t, err) + + _, conf2 := testServerConfig(t) + conf2.Bootstrap = false + server2, err := newServer(t, conf2) + require.NoError(t, err) + + // server1 is leader, server2 follower + testrpc.WaitForLeader(t, server1.RPC, "dc1") + joinLAN(t, server2, server1) + testrpc.WaitForLeader(t, server2.RPC, "dc1") + + peerId := testUUID() + + // Simulate a GenerateToken call on server1, which stores the establishment secret + { + require.NoError(t, server1.FSM().State().PeeringWrite(10, &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + Name: "foo", + ID: peerId, + }, + SecretsRequest: &pbpeering.SecretsWriteRequest{ + PeerID: peerId, + Request: &pbpeering.SecretsWriteRequest_GenerateToken{ + GenerateToken: &pbpeering.SecretsWriteRequest_GenerateTokenRequest{ + EstablishmentSecret: "389bbcdf-1c31-47d6-ae96-f2a3f4c45f84", + }, + }, + }, + })) + } + + testutil.RunStep(t, "server2 forwards write to server1", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + // We will dial server2 which should forward to server1 + conn, err := gogrpc.DialContext(ctx, server2.config.RPCAddr.String(), + gogrpc.WithContextDialer(newServerDialer(server2.config.RPCAddr.String())), + gogrpc.WithInsecure(), + gogrpc.WithBlock()) + require.NoError(t, err) + t.Cleanup(func() { conn.Close() }) + + peerStreamClient := pbpeerstream.NewPeerStreamServiceClient(conn) + req := &pbpeerstream.ExchangeSecretRequest{ + PeerID: peerId, + EstablishmentSecret: "389bbcdf-1c31-47d6-ae96-f2a3f4c45f84", + } + _, err = peerStreamClient.ExchangeSecret(ctx, req) + require.NoError(t, err) + }) +} From 6336d75da7bd16f540793597988171a19314e4cf Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Thu, 11 Aug 2022 10:26:21 -0700 Subject: [PATCH 247/339] ci: Disable Arm RPM verifications (#14142) --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d50b708c5..45891047dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -410,8 +410,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - arch: ["i386", "x86_64", "armv7hl", "aarch64"] - # fail-fast: true + # TODO(eculver): re-enable when there is a smaller verification container available + arch: ["i386", "x86_64"] #, "armv7hl", "aarch64"] env: version: ${{ needs.get-product-version.outputs.product-version }} From 0cf4afb18f92325c6954e0b4e7df235c3b511154 Mon Sep 17 00:00:00 2001 From: Mike Morris Date: Thu, 11 Aug 2022 13:32:34 -0400 Subject: [PATCH 248/339] docs(capigw): add v0.4.0 upgrade instructions (#14101) docs(capigw): add manual ReferencePolicy -> ReferenceGrant migration steps, comment out kube-storage-version-migrator workflow in case we choose to publish it later --- website/content/docs/api-gateway/upgrades.mdx | 159 +++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/upgrades.mdx b/website/content/docs/api-gateway/upgrades.mdx index 59381baa7b..24c29fde17 100644 --- a/website/content/docs/api-gateway/upgrades.mdx +++ b/website/content/docs/api-gateway/upgrades.mdx @@ -9,6 +9,163 @@ description: >- This topic describes how to upgrade Consul API Gateway. +## Upgrade to v0.4.0 + +Consul API Gateway v0.4.0 adds support for [Gateway API v0.5.0](https://github.com/kubernetes-sigs/gateway-api/releases/tag/v0.5.0) and the following resources: + +- The graduated v1beta1 `GatewayClass`, `Gateway` and `HTTPRoute` resources. + +- The [`ReferenceGrant`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant) resource, which replaces the identical [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) resource. + +Consul API Gateway v0.4.0 is backward-compatible with existing `ReferencePolicy` resources, but we will remove support for `ReferencePolicy` resources in a future release. We recommend that you migrate to `ReferenceGrant` after upgrading. + +### Requirements + +Ensure that the following requirements are met prior to upgrading: + +- Consul API Gateway should be running version v0.3.0. + +### Procedure + +1. Complete the [standard upgrade](#standard-upgrade). + +1. After completing the upgrade, complete the [post-upgrade configuration changes](#v0.4.0-post-upgrade-configuration-changes). The post-upgrade procedure describes how to replace your `ReferencePolicy` resources with `ReferenceGrant` resources and how to upgrade your `GatewayClass`, `Gateway`, and `HTTPRoute` resources from v1alpha2 to v1beta1. + +
+ +### Post-upgrade configuration changes +Complete the following steps after performing standard upgrade procedure. +#### Requirements + +- Consul API Gateway should be running version v0.4.0. +- Consul Helm chart should be v0.47.0 or later. +- You should have the ability to run `kubectl` CLI commands. +- `kubectl` should be configured to point to the cluster containing the installation you are upgrading. +- You should have the following permissions for your Kubernetes cluster: + - `Gateway.read` + - `ReferenceGrant.create` (Added in Consul Helm chart v0.47.0) + - `ReferencePolicy.delete` + +#### Procedure + +1. Verify the current version of the `consul-api-gateway-controller` `Deployment`: + + ```shell-session + $ kubectl get deployment --namespace consul consul-api-gateway-controller --output=jsonpath="{@.spec.template.spec.containers[?(@.name=='api-gateway-controller')].image}" + ``` + + You should receive a response similar to the following: + + ```log + "hashicorp/consul-api-gateway:0.4.0" + ``` + + + +1. Issue the following command to get all `ReferencePolicy` resources across all namespaces. + + ```shell-session + $ kubectl get referencepolicy --all-namespaces + ``` +If you have any active `ReferencePolicy` resources, you will receive output similar to the response below. + + ```log + Warning: ReferencePolicy has been renamed to ReferenceGrant. ReferencePolicy will be removed in v0.6.0 in favor of the identical ReferenceGrant resource. + NAMESPACE NAME + default example-reference-policy + ``` + + If your output is empty, upgrade your `GatewayClass`, `Gateway` and `HTTPRoute` resources to v1beta1 as described in [step 7](#v1beta1-gatewayclass-gateway-httproute). + +1. For each `ReferencePolicy` in the source YAML files, change the `kind` field to `ReferenceGrant`. You can optionally update the `metadata.name` field or filename if they include the term "policy". In the following example, the `kind` and `metadata.name` fields and filename have been changed to reflect the new resource. Note that updating the `kind` field prevents you from using the `kubectl edit` command to edit the remote state directly. + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: ReferenceGrant + metadata: + name: reference-grant + namespace: web-namespace + spec: + from: + - group: gateway.networking.k8s.io + kind: HTTPRoute + namespace: example-namesapce + to: + - group: "" + kind: Service + name: web-backend + ``` + + + +1. For each file, apply the updated YAML to your cluster to create a new `ReferenceGrant` resource. + + ```shell-session + $ kubectl apply --filename + ``` + +1. Check to confirm that each new `ReferenceGrant` was created successfully. + + ```shell-session + $ kubectl get referencegrant --namespace + NAME + example-reference-grant + ``` + +1. Finally, delete each corresponding old `ReferencePolicy` resource. Because replacement `ReferenceGrant` resources have already been created, there should be no interruption in the availability of any referenced `Service` or `Secret`. + + ```shell-session + $ kubectl delete referencepolicy --namespace + Warning: ReferencePolicy has been renamed to ReferenceGrant. ReferencePolicy will be removed in v0.6.0 in favor of the identical ReferenceGrant resource. + referencepolicy.gateway.networking.k8s.io "example-reference-policy" deleted + ``` + + + +1. For each `GatewayClass`, `Gateway`, and `HTTPRoute` in the source YAML, update the `apiVersion` field to `gateway.networking.k8s.io/v1beta1`. Note that updating the `apiVersion` field prevents you from using the `kubectl edit` command to edit the remote state directly. + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: example-gateway + namespace: gateway-namespace + spec: + ... + ``` + + + +1. For each file, apply the updated YAML to your cluster to update the existing `GatewayClass`, `Gateway` or `HTTPRoute` resources. + + ```shell-session + $ kubectl apply --filename + gateway.gateway.networking.k8s.io/example-gateway configured + ``` + + ## Upgrade to v0.3.0 from v0.2.0 or lower @@ -32,7 +189,7 @@ Ensure that the following requirements are met prior to upgrading: 1. Verify the current version of the `consul-api-gateway-controller` `Deployment`: ```shell-session - $ kubectl get deployment --namespace consul consul-api-gateway-controller --output=jsonpath= "{@.spec.template.spec.containers[?(@.name=='api-gateway-controller')].image}" + $ kubectl get deployment --namespace consul consul-api-gateway-controller --output=jsonpath="{@.spec.template.spec.containers[?(@.name=='api-gateway-controller')].image}" ``` You should receive a response similar to the following: From 605e5052e1440ac6c6cb1956f820b6b368076516 Mon Sep 17 00:00:00 2001 From: Tu Nguyen Date: Thu, 11 Aug 2022 11:17:17 -0700 Subject: [PATCH 249/339] Add upgrade instructions and considerations for Consul 1.13.1 --- website/content/docs/upgrading/instructions/index.mdx | 3 +++ website/content/docs/upgrading/upgrade-specific.mdx | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/website/content/docs/upgrading/instructions/index.mdx b/website/content/docs/upgrading/instructions/index.mdx index 9bae599d97..9b93fb2dd2 100644 --- a/website/content/docs/upgrading/instructions/index.mdx +++ b/website/content/docs/upgrading/instructions/index.mdx @@ -28,6 +28,9 @@ The upgrade guides will mention notable changes and link to relevant changelogs we recommend reviewing the changelog for versions between the one you are on and the one you are upgrading to at each step to familiarize yourself with changes. +~> **Note:** If you are upgrading from `1.11`+ and have connect proxies +registered, upgrade directly to `1.13.1` instead of `1.13.0`. + Select your _currently installed_ release series: - [1.9.x](/docs/upgrading/instructions/upgrade-to-1-10-x) - [1.8.x](/docs/upgrading/instructions/upgrade-to-1-10-x) diff --git a/website/content/docs/upgrading/upgrade-specific.mdx b/website/content/docs/upgrading/upgrade-specific.mdx index bf8c731259..13bd4973c1 100644 --- a/website/content/docs/upgrading/upgrade-specific.mdx +++ b/website/content/docs/upgrading/upgrade-specific.mdx @@ -14,7 +14,10 @@ provided for their upgrades as a result of new features or changed behavior. This page is used to document those details separately from the standard upgrade flow. -## Consul 1.13.0 +## Consul 1.13.x + +~> **Note:** If you are upgrading from `1.11`+ and have connect proxies +registered, upgrade directly to `1.13.1` instead of `1.13.0`. ### gRPC TLS From 4c928cb2f766ebcd55067b5a5658729a4f3cf00c Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Thu, 11 Aug 2022 14:47:10 -0400 Subject: [PATCH 250/339] Handle breaking change for ServiceVirtualIP restore (#14149) Consul 1.13.0 changed ServiceVirtualIP to use PeeredServiceName instead of ServiceName which was a breaking change for those using service mesh and wanted to restore their snapshot after upgrading to 1.13.0. This commit handles existing data with older ServiceName and converts it during restore so that there are no issues when restoring from older snapshots. --- .changelog/14149.txt | 3 ++ agent/consul/fsm/snapshot_oss.go | 40 ++++++++++++++++- agent/consul/fsm/snapshot_oss_test.go | 64 +++++++++++++++++++++++++++ agent/structs/structs.go | 4 +- 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 .changelog/14149.txt diff --git a/.changelog/14149.txt b/.changelog/14149.txt new file mode 100644 index 0000000000..726861f5a2 --- /dev/null +++ b/.changelog/14149.txt @@ -0,0 +1,3 @@ +```release-note:bug +agent: Fixed a compatibility issue when restoring snapshots from pre-1.13.0 versions of Consul [[GH-14107](https://github.com/hashicorp/consul/issues/14107)] +``` \ No newline at end of file diff --git a/agent/consul/fsm/snapshot_oss.go b/agent/consul/fsm/snapshot_oss.go index 167ffd100b..7fa53381a7 100644 --- a/agent/consul/fsm/snapshot_oss.go +++ b/agent/consul/fsm/snapshot_oss.go @@ -1,8 +1,12 @@ package fsm import ( + "fmt" + "net" + "github.com/hashicorp/consul-net-rpc/go-msgpack/codec" "github.com/hashicorp/raft" + "github.com/mitchellh/mapstructure" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" @@ -886,11 +890,43 @@ func restoreSystemMetadata(header *SnapshotHeader, restore *state.Restore, decod } func restoreServiceVirtualIP(header *SnapshotHeader, restore *state.Restore, decoder *codec.Decoder) error { - var req state.ServiceVirtualIP + // state.ServiceVirtualIP was changed in a breaking way in 1.13.0 (2e4cb6f77d2be36b02e9be0b289b24e5b0afb794). + // We attempt to reconcile the older type by decoding to a map then decoding that map into + // structs.PeeredServiceName first, and then structs.ServiceName. + var req struct { + Service map[string]interface{} + IP net.IP + + structs.RaftIndex + } if err := decoder.Decode(&req); err != nil { return err } - if err := restore.ServiceVirtualIP(req); err != nil { + + vip := state.ServiceVirtualIP{ + IP: req.IP, + RaftIndex: req.RaftIndex, + } + + // PeeredServiceName is the expected primary key type. + var psn structs.PeeredServiceName + if err := mapstructure.Decode(req.Service, &psn); err != nil { + return fmt.Errorf("cannot decode to structs.PeeredServiceName: %w", err) + } + vip.Service = psn + + // If the expected primary key field is empty, it must be the older ServiceName type. + if vip.Service.ServiceName.Name == "" { + var sn structs.ServiceName + if err := mapstructure.Decode(req.Service, &sn); err != nil { + return fmt.Errorf("cannot decode to structs.ServiceName: %w", err) + } + vip.Service = structs.PeeredServiceName{ + ServiceName: sn, + } + } + + if err := restore.ServiceVirtualIP(vip); err != nil { return err } return nil diff --git a/agent/consul/fsm/snapshot_oss_test.go b/agent/consul/fsm/snapshot_oss_test.go index b893c73bc7..2b2d3e8701 100644 --- a/agent/consul/fsm/snapshot_oss_test.go +++ b/agent/consul/fsm/snapshot_oss_test.go @@ -3,6 +3,7 @@ package fsm import ( "bytes" "fmt" + "net" "testing" "time" @@ -962,3 +963,66 @@ func TestFSM_BadSnapshot_NilCAConfig(t *testing.T) { require.EqualValues(t, 0, idx) require.Nil(t, config) } + +// This test asserts that ServiceVirtualIP, which made a breaking change +// in 1.13.0, can still restore from older snapshots which use the old +// state.ServiceVirtualIP type. +func Test_restoreServiceVirtualIP(t *testing.T) { + psn := structs.PeeredServiceName{ + ServiceName: structs.ServiceName{ + Name: "foo", + }, + } + + run := func(t *testing.T, input interface{}) { + t.Helper() + + var b []byte + buf := bytes.NewBuffer(b) + // Encode input + encoder := codec.NewEncoder(buf, structs.MsgpackHandle) + require.NoError(t, encoder.Encode(input)) + + // Create a decoder + dec := codec.NewDecoder(buf, structs.MsgpackHandle) + + logger := testutil.Logger(t) + fsm, err := New(nil, logger) + require.NoError(t, err) + + restore := fsm.State().Restore() + + // Call restore + require.NoError(t, restoreServiceVirtualIP(nil, restore, dec)) + require.NoError(t, restore.Commit()) + + ip, err := fsm.State().VirtualIPForService(psn) + require.NoError(t, err) + + // 240->224 due to addIPOffset + require.Equal(t, "224.0.0.2", ip) + } + + t.Run("new ServiceVirtualIP with PeeredServiceName", func(t *testing.T) { + run(t, state.ServiceVirtualIP{ + Service: psn, + IP: net.ParseIP("240.0.0.2"), + RaftIndex: structs.RaftIndex{}, + }) + }) + t.Run("pre-1.13.0 ServiceVirtualIP with ServiceName", func(t *testing.T) { + type compatServiceVirtualIP struct { + Service structs.ServiceName + IP net.IP + RaftIndex structs.RaftIndex + } + + run(t, compatServiceVirtualIP{ + Service: structs.ServiceName{ + Name: "foo", + }, + IP: net.ParseIP("240.0.0.2"), + RaftIndex: structs.RaftIndex{}, + }) + }) +} diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 4821b164c3..22fb47ca9d 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -2211,8 +2211,8 @@ type PeeredServiceName struct { } type ServiceName struct { - Name string - acl.EnterpriseMeta + Name string + acl.EnterpriseMeta `mapstructure:",squash"` } func NewServiceName(name string, entMeta *acl.EnterpriseMeta) ServiceName { From 39ec6e4c2b0d4d1a7405ac8cceb6771706c5cd7c Mon Sep 17 00:00:00 2001 From: Tu Nguyen Date: Thu, 11 Aug 2022 14:40:28 -0700 Subject: [PATCH 251/339] Apply suggestions from code review Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> --- website/content/docs/upgrading/upgrade-specific.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/website/content/docs/upgrading/upgrade-specific.mdx b/website/content/docs/upgrading/upgrade-specific.mdx index 13bd4973c1..56225fcefa 100644 --- a/website/content/docs/upgrading/upgrade-specific.mdx +++ b/website/content/docs/upgrading/upgrade-specific.mdx @@ -16,8 +16,14 @@ upgrade flow. ## Consul 1.13.x -~> **Note:** If you are upgrading from `1.11`+ and have connect proxies -registered, upgrade directly to `1.13.1` instead of `1.13.0`. +### Service Mesh Compatibility +Existing Consul deployments using service mesh (i.e., containing any registered Connect proxies) +should upgrade to **at least Consul 1.13.1**. + +Consul 1.13.0 contains a bug that prevents Consul server agents from restoring saved state +on startup if the state (1) was generated before Consul 1.13 (such as during an upgrade), +and (2) contained any Connect proxy registrations. +This bug is fixed in Consul versions 1.13.1 and newer. ### gRPC TLS From 10ee4d42fcec76b91bfa67bc9f9addd185f3796d Mon Sep 17 00:00:00 2001 From: Tu Nguyen Date: Thu, 11 Aug 2022 14:43:27 -0700 Subject: [PATCH 252/339] Update with more details on 1.13.0 issue --- .../content/docs/upgrading/instructions/index.mdx | 7 +++---- .../content/docs/upgrading/upgrade-specific.mdx | 14 +++++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/website/content/docs/upgrading/instructions/index.mdx b/website/content/docs/upgrading/instructions/index.mdx index 9b93fb2dd2..466acf335f 100644 --- a/website/content/docs/upgrading/instructions/index.mdx +++ b/website/content/docs/upgrading/instructions/index.mdx @@ -13,12 +13,14 @@ This document is intended to help users who find themselves many versions behind ## Upgrade Path Our recommended upgrade path is to move through the following sequence of versions: + - 0.8.5 (final 0.8.x) - 1.2.4 (final 1.2.x) - 1.6.10 (final 1.6.x) - 1.8.19 (final 1.8.x) - Latest 1.10.x -- Latest current version (1.11.x or 1.12.x) +- Latest 1.12.x +- Latest 1.13.x ([at least 1.13.1](/docs/upgrading/upgrade-specific#service-mesh-compatibility)) ## Getting Started @@ -28,9 +30,6 @@ The upgrade guides will mention notable changes and link to relevant changelogs we recommend reviewing the changelog for versions between the one you are on and the one you are upgrading to at each step to familiarize yourself with changes. -~> **Note:** If you are upgrading from `1.11`+ and have connect proxies -registered, upgrade directly to `1.13.1` instead of `1.13.0`. - Select your _currently installed_ release series: - [1.9.x](/docs/upgrading/instructions/upgrade-to-1-10-x) - [1.8.x](/docs/upgrading/instructions/upgrade-to-1-10-x) diff --git a/website/content/docs/upgrading/upgrade-specific.mdx b/website/content/docs/upgrading/upgrade-specific.mdx index 56225fcefa..75884adb2e 100644 --- a/website/content/docs/upgrading/upgrade-specific.mdx +++ b/website/content/docs/upgrading/upgrade-specific.mdx @@ -17,12 +17,16 @@ upgrade flow. ## Consul 1.13.x ### Service Mesh Compatibility -Existing Consul deployments using service mesh (i.e., containing any registered Connect proxies) -should upgrade to **at least Consul 1.13.1**. -Consul 1.13.0 contains a bug that prevents Consul server agents from restoring saved state -on startup if the state (1) was generated before Consul 1.13 (such as during an upgrade), -and (2) contained any Connect proxy registrations. +Existing Consul deployments using service mesh (i.e., containing any registered +Connect proxies) should upgrade to **at least Consul 1.13.1**. + +Consul 1.13.0 contains a bug that prevents Consul server agents from restoring +saved state on startup if the state + +1. was generated before Consul 1.13 (such as during an upgrade), and +2. contained any Connect proxy registrations. + This bug is fixed in Consul versions 1.13.1 and newer. ### gRPC TLS From 407b570e1f516f1d87a71db117ccf409ee965d54 Mon Sep 17 00:00:00 2001 From: DanStough Date: Thu, 11 Aug 2022 17:27:44 -0400 Subject: [PATCH 253/339] docs: changelog 1.12.4 and 1.11.8 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c47698349..08b8a37a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 1.12.4 (August 11, 2022) + +BUG FIXES: + +* cli: when `acl token read` is used with the `-self` and `-expanded` flags, return an error instead of panicking [[GH-13787](https://github.com/hashicorp/consul/issues/13787)] +* connect: Fixed a goroutine/memory leak that would occur when using the ingress gateway. [[GH-13847](https://github.com/hashicorp/consul/issues/13847)] +* connect: Ingress gateways with a wildcard service entry should no longer pick up non-connect services as upstreams. +connect: Terminating gateways with a wildcard service entry should no longer pick up connect services as upstreams. [[GH-13958](https://github.com/hashicorp/consul/issues/13958)] +* ui: Fixes an issue where client side validation errors were not showing in certain areas [[GH-14021](https://github.com/hashicorp/consul/issues/14021)] + +## 1.11.8 (August 11, 2022) + +BUG FIXES: + +* connect: Fixed a goroutine/memory leak that would occur when using the ingress gateway. [[GH-13847](https://github.com/hashicorp/consul/issues/13847)] +* connect: Ingress gateways with a wildcard service entry should no longer pick up non-connect services as upstreams. +connect: Terminating gateways with a wildcard service entry should no longer pick up connect services as upstreams. [[GH-13958](https://github.com/hashicorp/consul/issues/13958)] + ## 1.13.0 (August 9, 2022) BREAKING CHANGES: From 05a9af71e374e799263428692d39c1eabcf8edb6 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Thu, 11 Aug 2022 16:23:02 -0700 Subject: [PATCH 254/339] Add changelog entry for peering fix (#14160) --- .changelog/14119.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/14119.txt diff --git a/.changelog/14119.txt b/.changelog/14119.txt new file mode 100644 index 0000000000..f0958361bd --- /dev/null +++ b/.changelog/14119.txt @@ -0,0 +1,3 @@ +```release-note:bug +connect: Fixed some spurious issues during peering establishment when a follower is dialed +``` From a2c0494605b8a3312ad9fee0035e4be444e361e4 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Thu, 11 Aug 2022 16:24:30 -0700 Subject: [PATCH 255/339] docs: changelog for 1.13.1 (#14168) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b8a37a70..b92ca84f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.13.1 (August 11, 2022) + +BUG FIXES: + +* agent: Fixed a compatibility issue when restoring snapshots from pre-1.13.0 versions of Consul [[GH-14107](https://github.com/hashicorp/consul/issues/14107)] [[GH-14149](https://github.com/hashicorp/consul/issues/14149)] +* connect: Fixed some spurious issues during peering establishment when a follower is dialed [[GH-14119](https://github.com/hashicorp/consul/issues/14119)] + ## 1.12.4 (August 11, 2022) BUG FIXES: From 81931e52c3a0ba920af6e89ae2ea56ff64147c3c Mon Sep 17 00:00:00 2001 From: cskh Date: Thu, 11 Aug 2022 22:09:56 -0400 Subject: [PATCH 256/339] feat(telemetry): add labels to serf and memberlist metrics (#14161) * feat(telemetry): add labels to serf and memberlist metrics * changelog * doc update Co-authored-by: R.B. Boyer <4903+rboyer@users.noreply.github.com> --- .changelog/14161.txt | 3 +++ agent/consul/client_serf.go | 2 ++ agent/consul/config.go | 2 ++ agent/consul/server_oss.go | 15 +++++++++++++++ agent/consul/server_serf.go | 10 +++++++--- go.mod | 6 +++--- go.sum | 10 ++++++---- website/content/docs/agent/telemetry.mdx | 4 ++++ 8 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 .changelog/14161.txt diff --git a/.changelog/14161.txt b/.changelog/14161.txt new file mode 100644 index 0000000000..2926ffbe9b --- /dev/null +++ b/.changelog/14161.txt @@ -0,0 +1,3 @@ +```release-note:improvement +metrics: add labels of segment, partition, network area, network (lan or wan) to serf and memberlist metrics +``` diff --git a/agent/consul/client_serf.go b/agent/consul/client_serf.go index 55df7a5471..05db21e2f3 100644 --- a/agent/consul/client_serf.go +++ b/agent/consul/client_serf.go @@ -62,6 +62,8 @@ func (c *Client) setupSerf(conf *serf.Config, ch chan serf.Event, path string) ( return nil, err } + addSerfMetricsLabels(conf, false, "", "", "") + addEnterpriseSerfTags(conf.Tags, c.config.AgentEnterpriseMeta()) conf.ReconnectTimeoutOverride = libserf.NewReconnectOverride(c.logger) diff --git a/agent/consul/config.go b/agent/consul/config.go index b897c4f232..38063f808a 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -584,6 +584,7 @@ func CloneSerfLANConfig(base *serf.Config) *serf.Config { cfg.MemberlistConfig.ProbeTimeout = base.MemberlistConfig.ProbeTimeout cfg.MemberlistConfig.SuspicionMult = base.MemberlistConfig.SuspicionMult cfg.MemberlistConfig.RetransmitMult = base.MemberlistConfig.RetransmitMult + cfg.MemberlistConfig.MetricLabels = base.MemberlistConfig.MetricLabels // agent/keyring.go cfg.MemberlistConfig.Keyring = base.MemberlistConfig.Keyring @@ -593,6 +594,7 @@ func CloneSerfLANConfig(base *serf.Config) *serf.Config { cfg.ReapInterval = base.ReapInterval cfg.TombstoneTimeout = base.TombstoneTimeout cfg.MemberlistConfig.SecretKey = base.MemberlistConfig.SecretKey + cfg.MetricLabels = base.MetricLabels return cfg } diff --git a/agent/consul/server_oss.go b/agent/consul/server_oss.go index 5ae2fc3ea6..4ae524b65c 100644 --- a/agent/consul/server_oss.go +++ b/agent/consul/server_oss.go @@ -159,3 +159,18 @@ func (s *Server) addEnterpriseStats(stats map[string]map[string]string) { func getSerfMemberEnterpriseMeta(member serf.Member) *acl.EnterpriseMeta { return structs.NodeEnterpriseMetaInDefaultPartition() } + +func addSerfMetricsLabels(conf *serf.Config, wan bool, segment string, partition string, areaID string) { + conf.MetricLabels = []metrics.Label{} + + networkMetric := metrics.Label{ + Name: "network", + } + if wan { + networkMetric.Value = "wan" + } else { + networkMetric.Value = "lan" + } + + conf.MetricLabels = append(conf.MetricLabels, networkMetric) +} diff --git a/agent/consul/server_serf.go b/agent/consul/server_serf.go index 5e29b47dd2..b9c8ad95f9 100644 --- a/agent/consul/server_serf.go +++ b/agent/consul/server_serf.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" "github.com/hashicorp/memberlist" "github.com/hashicorp/raft" @@ -177,9 +178,10 @@ func (s *Server) setupSerfConfig(opts setupSerfOptions) (*serf.Config, error) { if opts.WAN { nt, err := memberlist.NewNetTransport(&memberlist.NetTransportConfig{ - BindAddrs: []string{conf.MemberlistConfig.BindAddr}, - BindPort: conf.MemberlistConfig.BindPort, - Logger: conf.MemberlistConfig.Logger, + BindAddrs: []string{conf.MemberlistConfig.BindAddr}, + BindPort: conf.MemberlistConfig.BindPort, + Logger: conf.MemberlistConfig.Logger, + MetricLabels: []metrics.Label{{Name: "network", Value: "wan"}}, }) if err != nil { return nil, err @@ -230,6 +232,8 @@ func (s *Server) setupSerfConfig(opts setupSerfOptions) (*serf.Config, error) { conf.ReconnectTimeoutOverride = libserf.NewReconnectOverride(s.logger) + addSerfMetricsLabels(conf, opts.WAN, "", "", "") + addEnterpriseSerfTags(conf.Tags, s.config.AgentEnterpriseMeta()) if s.config.OverrideInitialSerfTags != nil { diff --git a/go.mod b/go.mod index cb048763d6..e2fbafed4b 100644 --- a/go.mod +++ b/go.mod @@ -45,11 +45,11 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hil v0.0.0-20200423225030-a18a1cd20038 - github.com/hashicorp/memberlist v0.3.1 + github.com/hashicorp/memberlist v0.4.0 github.com/hashicorp/raft v1.3.9 github.com/hashicorp/raft-autopilot v0.1.6 github.com/hashicorp/raft-boltdb/v2 v2.2.2 - github.com/hashicorp/serf v0.9.8 + github.com/hashicorp/serf v0.10.0 github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 @@ -77,7 +77,7 @@ require ( golang.org/x/net v0.0.0-20211216030914-fe4d6282115f golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e google.golang.org/genproto v0.0.0-20200623002339-fbb79eadd5eb google.golang.org/grpc v1.37.1 diff --git a/go.sum b/go.sum index 8f2afaa45d..ee3e0beda1 100644 --- a/go.sum +++ b/go.sum @@ -364,8 +364,9 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/memberlist v0.3.1 h1:MXgUXLqva1QvpVEDQW1IQLG0wivQAtmFlHRQ+1vWZfM= github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.4.0 h1:k3uda5gZcltmafuFF+UFqNEl5PrH+yPZ4zkjp1f/H/8= +github.com/hashicorp/memberlist v0.4.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= @@ -380,8 +381,8 @@ github.com/hashicorp/raft-boltdb v0.0.0-20211202195631-7d34b9fb3f42/go.mod h1:wc github.com/hashicorp/raft-boltdb/v2 v2.2.2 h1:rlkPtOllgIcKLxVT4nutqlTH2NRFn+tO1wwZk/4Dxqw= github.com/hashicorp/raft-boltdb/v2 v2.2.2/go.mod h1:N8YgaZgNJLpZC+h+by7vDu5rzsRgONThTEeUS3zWbfY= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/serf v0.9.8 h1:JGklO/2Drf1QGa312EieQN3zhxQ+aJg6pG+aC3MFaVo= -github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.10.0 h1:89qvvpfMQnz6c2y4pv7j2vUUmeT1+5TSZMexuTbtsPs= +github.com/hashicorp/serf v0.10.0/go.mod h1:bXN03oZc5xlH46k/K1qTrpXb9ERKyY1/i/N5mxvgrZw= github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 h1:OKsyxKi2sNmqm1Gv93adf2AID2FOBFdCbbZn9fGtIdg= github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM= @@ -793,8 +794,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/website/content/docs/agent/telemetry.mdx b/website/content/docs/agent/telemetry.mdx index 575f3d7e5b..8b4f923435 100644 --- a/website/content/docs/agent/telemetry.mdx +++ b/website/content/docs/agent/telemetry.mdx @@ -605,6 +605,10 @@ Any metric in this section can be turned off with the [`prefix_filter`](/docs/ag ## Cluster Health These metrics give insight into the health of the cluster as a whole. +Query for the `consul.memberlist.*` and `consul.serf.*` metrics can be appended +with certain labels to further distinguish data between different gossip pools. +The supported label for OSS is `network`, while `segment`, `partition`, `area` +are allowed for . | Metric | Description | Unit | Type | |----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------|---------| From 1610fb0315344dd4eea4149b9ede05c8ba816b50 Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Thu, 11 Aug 2022 20:10:36 -0700 Subject: [PATCH 257/339] updated Routes configuration ref --- .../docs/api-gateway/configuration/routes.mdx | 107 +++++++++++++++++- 1 file changed, 104 insertions(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index 7819d482f6..199dbff6de 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -7,7 +7,9 @@ description: >- # Route -Routes are independent configuration objects that are associated with specific listeners. +This topic describes how to create and configure `Route` resources. Routes are independent configuration objects that are associated with specific listeners. + +## Create a `Route` Declare a route with either `kind: HTTPRoute` or `kind: TCPRoute` and configure the route parameters in the `spec` block. Refer to the Kubernetes Gateway API documentation for each object type for details: @@ -36,8 +38,55 @@ The following example creates a route named `example-route` associated with a li -To create a route for a `backendRef` in a different namespace, you must also -create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy). +## Configuration model + +The following outline shows how to format the configurations for the `Route` object. The top-level `spec` field is the root for all configurations. Click on a property name to view details about the configuration. + +## Specification + +This topic provides details about the configuration parameters. + +### parentRefs + +This field contains the list of `Gateways` that the route should attach to. If not set, the route will not attach to a `Gateway`. + +* Type: List of objects +* Required: Required + +### parentRefs.name + +This field specifies the name of the `Gateway` the route is attached to. + +* Type: String +* Required: Required + +### ParentRefs.namespace + +This field specifies the Kubernetes namespace containing the `Gateway` to attach to. It is optional if the `Gateway` is in the same Kubernetes namespace as the `Route`. If the `Gateway` is in a different namespace, then a value must be provided. + +* Type: String +* Required: Optional + +### rules + +The `rules` field specifies how traffic passing through the route should behave. It contains several possible parameters to customize traffic behavior. + +* Type: List of objects +* Required: Required + +### rules.backendRefs + +This field specifies backend services that the `Route` references. The following table describes the parameters for `backendRefs`: + +| Parameter | Description | Type | Required | +| --- | --- | --- | --- | +| `group` | Specifies the Kubernetes API Group of the referenced backend. You can specify the following values:

| String | Optional | +| `kind` | Specifies the Kubernetes Kind of the referenced backend. You can specify the following values:
  • `Service`: Indicates that the `backendRef` references a Service in the Kubernetes cluster. This is the default value if unspecified.
  • `MeshService`: Indicates that the `backendRef` references a service in the Consul mesh.
| String | Optional | +| `name` | Specifies the name of the Kubernetes Service or Consul mesh service resource. | String | Required | +| `namespace` | Specifies the Kubernetes namespace containing the Kubernetes Service or Consul mesh service resource. To create a route for a `backendRef` in a different namespace, you must also create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy). Refer to the [example route](#example-cross-namespace-backendref) configured to reference across namespaces. | String | Optional | +| `port` | Specifies the port number for accessing the Kubernetes or Consul service. | Integer | Required | + +#### Example cross-namespace backendRef The following example creates a route named `example-route` in namespace `gateway-namespace`. This route has a `backendRef` in namespace `service-namespace`. Traffic is allowed because the `ReferencePolicy`, named `reference-policy` in namespace `service-namespace`, allows traffic from `HTTPRoutes` in `gateway-namespace` to `Services` in `service-namespace`. @@ -78,6 +127,57 @@ The following example creates a route named `example-route` in namespace `gatewa +### rules.filters + +The `filters` block defines steps for processing requests. You can configure filters to modify the properties of matching incoming requests and enable Consul API Gateway features, such as rewriting path prefixes (refer to [Rerouting HTTP](/docs/api-gateway/usage#rerouting-http-requests) for additional information). + +* Type: Array of objects +* Required: Optional + +### rules.filters.type + +Specifies the type of filter you want to apply to the route. The parameter is optional and takes a string value. + +You can specify the following values: + +* `RequestHeaderModifier`: The `RequestHeaderModifier` type modifies the HTTP headers on the incoming request. +* `URLRewrite`: The `URLRewrite` type modifies the URL path on the incoming request. + +### rules.filters.requestHeaderModifier + +Contains a list of header configuration objects for `requestHeaderModifier` filters when `rules.filters.type` is configured to `RequestHeaderModifier`. + +### rules.filters.urlRewrite + +Contains a list of path configuration objects for `urlRewrite` filters when `rules.filters.type` is configured to `URLRewrite`. + +### rules.filters.urlRewrite.path + +Specifies a list of objects that determine how Consul API Gateway rewrites URL paths (refer to [Rerouting HTTP](/docs/api-gateway/usage#rerouting-http-requests) for additional information). + +The following table describes the parameters for `path`: + +| Parameter | Description | Type | Required | +| --- | --- | --- | --- | +| `replacePrefixMatch` | Specifies the path prefix to use as the replacement when rerouting requests. | String | Required | +| `type` | Specifies the type of rewrite rule. You can specify the following values:
  • `ReplacePrefixMatch`
| String | Optional | + +### rules.matches + +Specifies rules for matching incoming requests. You can apply [`filters`](#rulesfilters) to requests that match the defined rules. + +### rules.matches.path + +Specifies a list of objects that define pattern-matching rules. Consul API Gateway processes matching requests according to the rules configured for the routes. + +The following table describes the parameters for `path`: + +| Parameter | Description | Type | Required | +| --- | --- | --- | --- | +| `type` | | String | Required | +| `value` | | String | Required | + + \ No newline at end of file From 54e7924943685a9c3a26114505fbea33ce078e9e Mon Sep 17 00:00:00 2001 From: trujillo-adam Date: Thu, 11 Aug 2022 20:19:51 -0700 Subject: [PATCH 258/339] added usage docs for prefix rewrite --- website/content/docs/api-gateway/usage.mdx | 54 ++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/usage.mdx b/website/content/docs/api-gateway/usage.mdx index e89c0556fc..5ba7cfc808 100644 --- a/website/content/docs/api-gateway/usage.mdx +++ b/website/content/docs/api-gateway/usage.mdx @@ -2,13 +2,17 @@ layout: docs page_title: Consul API Gateway Basic Usage description: >- - Consul API Gateway Basic Usage + This topic describes how to use Consul API Gateway. --- -# Basic Usage +# Usage -This topic describes the basic workflow for implementing Consul API Gateway configurations. +This topic describes how to use Consul API Gateway. + +## Basic usage + +Complete the following steps to use Consul API Gateway in your network. 1. Verify that the [requirements](/docs/api-gateway/tech-specs) have been met. 1. Verify that the Consul API Gateway CRDs and controller have been installed and applied (see [Installation](/docs/api-gateway/consul-api-gateway-install)). @@ -30,6 +34,50 @@ This topic describes the basic workflow for implementing Consul API Gateway conf $ kubectl apply -f gateway.yaml routes.yaml ``` +## Reroute HTTP requests + +Configure the following fields in your `Route` configuration to use this feature. Refer to the [Route configuration reference](/docs/api-gateway/configuration/routes) for details about the parameters. + +* [`rules.filters.type`](/docs/api-gateway/configuration/routes#rules-filters-type): Set this parameter to `URLRewrite` to instruct Consul API Gateway to rewrite the URL when specific conditions are met. +* [`rules.filters.urlRewrite`](/docs/api-gateway/configuration/routes#rules-filters-urlrewrite): Specify the `path` configuration. +* [`rules.filters.urlRewrite.path`](/docs/api-gateway/configuration/routes#rules-filters-urlrewrite-path): Contains the paths that incoming requests should be rewritten to based on the match conditions. + +Note that if the route is configured to accept paths with and without a trailing slash, you must make two separate routes to handle each case. + +### Example + +In the following example, requests to` /incoming-request-prefix/` are forwarded to the `backendRef` as `/prefix-backend-receives/`. A request to `/incoming-request-prefix/request-path`, for instance, is received by the `backendRef` as `/prefix-backend-receives/request-path`. + + + +```yaml hideClipboard +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: example-route + ##... +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: api-gateway + rules: + - backendRefs: + . . . + filters: + - type: URLRewrite + urlRewrite: + path: + replacePrefixMatch: /prefix-backend-receives/ + type: ReplacePrefixMatch + matches: + - path: + type: PathPrefix + value: /incoming–request-prefix/ +``` + + + \ No newline at end of file From c10464fc0ba26d1d073f9480f7e0dc7f159ec317 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 15 Aug 2022 11:49:41 -0400 Subject: [PATCH 274/339] Add missing code block --- website/content/docs/api-gateway/configuration/meshservice.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/meshservice.mdx b/website/content/docs/api-gateway/configuration/meshservice.mdx index 49c6621fa3..ef085f2f5e 100644 --- a/website/content/docs/api-gateway/configuration/meshservice.mdx +++ b/website/content/docs/api-gateway/configuration/meshservice.mdx @@ -7,7 +7,7 @@ description: >- # MeshService -This topic provides full details about the MeshService resource. +This topic provides full details about the `MeshService` resource. ## Introduction From b6e1962265c84a52b7cf18017e3d1e75e37bf653 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 15 Aug 2022 11:56:54 -0400 Subject: [PATCH 275/339] Update ReferencePolicy -> ReferenceGrant --- .../content/docs/api-gateway/configuration/gateway.mdx | 2 +- .../content/docs/api-gateway/configuration/routes.mdx | 10 +++++----- website/content/docs/api-gateway/index.mdx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 94b4808458..4d07c8e989 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -159,7 +159,7 @@ Specifies the `tls` configurations for the `Gateway`. The `tls` object is requir | Parameter | Description | Type | Required | | --- | --- | --- | --- | -| `certificateRefs` |
Specifies Kubernetes `name` and `namespace` objects that contains TLS certificates and private keys.
The certificates establish a TLS handshake for requests that match the `hostname` of the associated `listener`. Each reference must be a Kubernetes Secret. If you are using a Secret in a namespace other than the `Gateway`'s, each reference must also have a corresponding [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy).
| Object or array | Required if `tls` is set | +| `certificateRefs` |
Specifies Kubernetes `name` and `namespace` objects that contains TLS certificates and private keys.
The certificates establish a TLS handshake for requests that match the `hostname` of the associated `listener`. Each reference must be a Kubernetes Secret. If you are using a Secret in a namespace other than the `Gateway`'s, each reference must also have a corresponding [`ReferenceGrant`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant).
| Object or array | Required if `tls` is set | | `mode` | Specifies the TLS Mode. Should always be set to `Terminate` for `HTTPRoutes` | string | Required if `certificateRefs` is set | | `options` | Specifies additional Consul API Gateway options. | Map of strings | optional | diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index 7819d482f6..3822cf0f45 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -37,11 +37,11 @@ The following example creates a route named `example-route` associated with a li To create a route for a `backendRef` in a different namespace, you must also -create a [ReferencePolicy](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy). +create a [ReferenceGrant](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant). -The following example creates a route named `example-route` in namespace `gateway-namespace`. This route has a `backendRef` in namespace `service-namespace`. Traffic is allowed because the `ReferencePolicy`, named `reference-policy` in namespace `service-namespace`, allows traffic from `HTTPRoutes` in `gateway-namespace` to `Services` in `service-namespace`. +The following example creates a route named `example-route` in namespace `gateway-namespace`. This route has a `backendRef` in namespace `service-namespace`. Traffic is allowed because the `ReferenceGrant`, named `reference-grant` in namespace `service-namespace`, allows traffic from `HTTPRoutes` in `gateway-namespace` to `Services` in `service-namespace`. - + ```yaml apiVersion: gateway.networking.k8s.io/v1alpha2 @@ -61,9 +61,9 @@ The following example creates a route named `example-route` in namespace `gatewa --- apiVersion: gateway.networking.k8s.io/v1alpha2 - kind: ReferencePolicy + kind: ReferenceGrant metadata: - name: reference-policy + name: reference-grant namespace: service-namespace spec: from: diff --git a/website/content/docs/api-gateway/index.mdx b/website/content/docs/api-gateway/index.mdx index 2c32c5abdd..6a811fd71b 100644 --- a/website/content/docs/api-gateway/index.mdx +++ b/website/content/docs/api-gateway/index.mdx @@ -38,7 +38,7 @@ are used, see the [documentation in our GitHub repo](https://github.com/hashicor | [`Gateway`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Gateway) |
  • Supported protocols: `HTTP`, `HTTPS`, `TCP`
  • Header-based hostname matching (no SNI support)
  • Supported filters: header addition, removal, and setting
  • TLS modes supported: `terminate`
  • Certificate types supported: `core/v1/Secret`
  • Extended options: TLS version and cipher constraints
| | [`HTTPRoute`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute) |
  • Weight-based load balancing
  • Supported rules: path, header, query, and method-based matching
  • Supported filters: header addition, removal, and setting
  • Supported backend types:
    1. `core/v1/Service` (must map to a registered Consul service)
    2. `api-gateway.consul.hashicorp.com/v1alpha1/MeshService`
| | [`TCPRoute`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute) |
  • Supported backend types:
    1. `core/v1/Service` (must map to a registered Consul service)
    2. `api-gateway.consul.hashicorp.com/v1alpha1/MeshService`
| -| [`ReferencePolicy`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy) |
  • Required to allow any reference from a `Gateway` to a Kubernetes `core/v1/Secret` in a different namespace.
    • A Gateway with an unpermitted `certificateRefs` caused by the lack of a` ReferencePolicy` sets a `ResolvedRefs` status to `False` with the reason `InvalidCertificateRef`. The Gateway will not become ready in this case.
  • Required to allow any reference from an `HTTPRoute` or `TCPRoute` to a Kubernetes `core/v1/Service` in a different namespace.
    • A route with an unpermitted `backendRefs` caused by the lack of a `ReferencePolicy` sets a `ResolvedRefs` status to `False` with the reason `RefNotPermitted`. The gateway listener rejects routes with an unpermitted `backendRefs`.
    • WARNING: If a route `backendRefs` becomes unpermitted, the entire route is removed from the gateway listener.
      • A `backendRefs` can become unpermitted when you delete a `ReferencePolicy` or add a new unpermitted `backendRefs` to an existing route.
| +| [`ReferenceGrant`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferenceGrant) |
  • Required to allow any reference from a `Gateway` to a Kubernetes `core/v1/Secret` in a different namespace.
    • A Gateway with an unpermitted `certificateRefs` caused by the lack of a` ReferenceGrant` sets a `ResolvedRefs` status to `False` with the reason `InvalidCertificateRef`. The Gateway will not become ready in this case.
  • Required to allow any reference from an `HTTPRoute` or `TCPRoute` to a Kubernetes `core/v1/Service` in a different namespace.
    • A route with an unpermitted `backendRefs` caused by the lack of a `ReferenceGrant` sets a `ResolvedRefs` status to `False` with the reason `RefNotPermitted`. The gateway listener rejects routes with an unpermitted `backendRefs`.
    • WARNING: If a route `backendRefs` becomes unpermitted, the entire route is removed from the gateway listener.
      • A `backendRefs` can become unpermitted when you delete a `ReferenceGrant` or add a new unpermitted `backendRefs` to an existing route.
| ## Additional Resources From 01a34917cdb6ebe16b8d4cd30a543146dae51bbf Mon Sep 17 00:00:00 2001 From: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:02:46 -0700 Subject: [PATCH 276/339] Apply suggestions from code review Co-authored-by: Nathan Coleman --- .../content/docs/api-gateway/configuration/meshservice.mdx | 2 +- website/content/docs/api-gateway/configuration/routes.mdx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/meshservice.mdx b/website/content/docs/api-gateway/configuration/meshservice.mdx index ef085f2f5e..0307b3a4be 100644 --- a/website/content/docs/api-gateway/configuration/meshservice.mdx +++ b/website/content/docs/api-gateway/configuration/meshservice.mdx @@ -11,7 +11,7 @@ This topic provides full details about the `MeshService` resource. ## Introduction -A `MeshService` is a resource in the Kubernetes cluster that represents a service in the Consul service mesh outside the Kubernetes cluster where Consul API Gateway is deployed. The service must be in the same Consul datacenter. The MeshService exists so that other configuration models in Kubernetes, such as HTTPRoute and TCPRoute, can reference services that only exist in Consul. +A `MeshService` is a resource in the Kubernetes cluster that enables Kubernetes configuration models, such as `HTTPRoute` and `TCPRoute`, to reference services that only exist in Consul. A `MeshService` represents a service in the Consul service mesh outside the Kubernetes cluster where Consul API Gateway is deployed. The service represented by the `MeshService` resource must be in the same Consul datacenter as the Kubernetes cluster. ## Configuration Model diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index 58a07758d3..acce1d68e8 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -220,7 +220,7 @@ Specifies rules for matching incoming requests. You can apply [`filters`](#rules * [headers](#rules-matches-headers) * [query parameters](#rules-matches-queryparams) * [request method](#rules-matches-method) - +Each rule matches requests independently. As a result, a request matching any of the conditions is considered a match. You can configure several matching rules for each type to widen or narrow matches. ### rules.matches.path Specifies a list of objects that define matches based on URL path. The following table describes the parameters for the `path` field: @@ -228,7 +228,7 @@ Specifies a list of objects that define matches based on URL path. The following | Parameter | Description | Type | Required | | --- | --- | --- | --- | | `type` | Specifies the type of comparison to use for matching the path value. You can specify the following types.
  • `Exact`: Returns a match only when the entire path matches the `value` field (default).
  • `PathPrefix`: Returns a match when the path matches the regex defined in the `value` field.
| String | Required | -| `value` | Specifies value to match on. You can specify a specific string or a regular expression. | String | Required | +| `value` | Specifies the value to match on. You can specify a specific string when `type` is `Exact` or `PathPrefix`. You can specify a regular expression if `type` is `RegularExpression`. | String | Required | ### rules.matches.headers From 10b89163e48123eb95686bce9ede05595cf1a408 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 15 Aug 2022 17:13:44 -0400 Subject: [PATCH 277/339] Apply suggestions from code review --- website/content/docs/api-gateway/configuration/routes.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index acce1d68e8..fc41f85d52 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -220,6 +220,7 @@ Specifies rules for matching incoming requests. You can apply [`filters`](#rules * [headers](#rules-matches-headers) * [query parameters](#rules-matches-queryparams) * [request method](#rules-matches-method) + Each rule matches requests independently. As a result, a request matching any of the conditions is considered a match. You can configure several matching rules for each type to widen or narrow matches. ### rules.matches.path @@ -227,7 +228,7 @@ Specifies a list of objects that define matches based on URL path. The following | Parameter | Description | Type | Required | | --- | --- | --- | --- | -| `type` | Specifies the type of comparison to use for matching the path value. You can specify the following types.
  • `Exact`: Returns a match only when the entire path matches the `value` field (default).
  • `PathPrefix`: Returns a match when the path matches the regex defined in the `value` field.
| String | Required | +| `type` | Specifies the type of comparison to use for matching the path value. You can specify the following types.
  • `Exact`: Returns a match only when the entire path matches the `value` field (default).
  • `PathPrefix`: Returns a match when the path has the prefix defined in the `value` field.
  • `RegularExpression`: Returns a match when the path matches the regex defined in the `value` field.
| String | Required | | `value` | Specifies the value to match on. You can specify a specific string when `type` is `Exact` or `PathPrefix`. You can specify a regular expression if `type` is `RegularExpression`. | String | Required | ### rules.matches.headers From 23ef639c6da16463990e208bb5789cbd25d2dab8 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 15 Aug 2022 17:21:36 -0400 Subject: [PATCH 278/339] Fix typo --- website/content/docs/api-gateway/configuration/routes.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/routes.mdx b/website/content/docs/api-gateway/configuration/routes.mdx index fc41f85d52..dced0cfa04 100644 --- a/website/content/docs/api-gateway/configuration/routes.mdx +++ b/website/content/docs/api-gateway/configuration/routes.mdx @@ -191,7 +191,7 @@ Defines operations to perform on matching request headers when `rules.filters.ty | --- | --- | --- | --- | | `set` | Configure this field to rewrite the HTTP request header. It specifies the name of an HTTP header to overwrite and the new value to set. Any existing values associated with the header name are overwritten. You can specify the following configurations:
  • `name`: Required string that specifies the name of the HTTP header to set.
  • `value`: Required string that specifies the value of the HTTP header to set.
| List of objects | Optional | | `add` | Configure this field to append the request header with a new value. It specifies the name of an HTTP header to append and the value(s) to add. You can specify the following configurations:
  • `name`: Required string that specifies the name of the HTTP header to append.
  • `value`: Required string that specifies the value of the HTTP header to add.
| List of objects | Optional | -| `remove` | Configure this field to specifify an array of header names to remove from the request header. | Array of strings | Optional | +| `remove` | Configure this field to specify an array of header names to remove from the request header. | Array of strings | Optional | ### rules.filters.urlRewrite From cb0e64db86d45012151c8b08845032183c33cd46 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:52:47 -0500 Subject: [PATCH 279/339] Update redirects.js --- website/redirects.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/redirects.js b/website/redirects.js index 52a313b046..583b2a5d17 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -1265,7 +1265,12 @@ module.exports = [ }, { source: '/docs/api-gateway/api-gateway-usage', - destination: '/docs/api-gateway/consul-api-gateway-install', + destination: '/docs/api-gateway/install', + permanent: true, + }, + { + source: '/docs/api-gateway/api-gateway/consul-api-gateway-install', + destination: '/docs/api-gateway/install', permanent: true, }, { From 152cfdf6dc46f74ff341c8d4129b217e7f5ae3e5 Mon Sep 17 00:00:00 2001 From: sarahalsmiller <100602640+sarahalsmiller@users.noreply.github.com> Date: Mon, 15 Aug 2022 18:00:08 -0500 Subject: [PATCH 280/339] Update redirects.js --- website/redirects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/redirects.js b/website/redirects.js index 583b2a5d17..a4b3f272bb 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -1269,7 +1269,7 @@ module.exports = [ permanent: true, }, { - source: '/docs/api-gateway/api-gateway/consul-api-gateway-install', + source: '/docs/api-gateway/consul-api-gateway-install', destination: '/docs/api-gateway/install', permanent: true, }, From d79368a866e72a9cdbc4ee9267bf01414ffa8bfb Mon Sep 17 00:00:00 2001 From: Jeff Apple <79924108+Jeff-Apple@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:26:02 -0700 Subject: [PATCH 281/339] Update website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx --- .../content/docs/release-notes/consul-api-gateway/v0_4_x.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx index c14f995598..382e882849 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx @@ -34,7 +34,7 @@ description: >- `//cart/checkout`. Please see the product documentation for details on how to configure this feature. -## What's Changed +## What Has Changed - **Reference Policy Renamed to Reference Grant** In v0.5.0 of the Kubernetes Gateway API, `ReferencePolicy` has been renamed to `ReferenceGrant`. This From 081afac966d1006a6d3fca0732c3f0f277f7efdd Mon Sep 17 00:00:00 2001 From: Jeff Apple <79924108+Jeff-Apple@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:31:46 -0700 Subject: [PATCH 282/339] Minor edits to Release Notes Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- .../docs/release-notes/consul-api-gateway/v0_4_x.mdx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx index 382e882849..3512cb0d95 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx @@ -27,12 +27,8 @@ description: >- - **URL Path Prefix Rewrite** This release introduces support for rewriting a URL's path prefix when routing - HTTP traffic. This is configured by adding a `URLRewrite` filter to a - `HTTPRoute`. With this feature, the gateway can rewrite the URL path, in a - client's HTTP Request, before sending the request to a service. A simple - example of this is changing the path from `//store/checkout` to - `//cart/checkout`. Please see the product documentation for details on how to - configure this feature. + HTTP traffic. To use this functionality, add a `URLRewrite` filter to an + `HTTPRoute` configuration. This enables the gateway to rewrite the URL path in a client's HTTP request before sending the request to a service. For example, you could configure the gateway to change the path from `//store/checkout` to `//cart/checkout`. Refer to the [usage documentation](/docs/api-gateway/usage) for additional information. ## What Has Changed From 3dcd60f8056158825e5083cec748ce3dd1e72917 Mon Sep 17 00:00:00 2001 From: Jeff Apple <79924108+Jeff-Apple@users.noreply.github.com> Date: Tue, 16 Aug 2022 09:36:23 -0700 Subject: [PATCH 283/339] Minor edits to Release Notes --- .../content/docs/release-notes/consul-api-gateway/v0_4_x.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx index 3512cb0d95..4f440dc249 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx @@ -38,7 +38,7 @@ description: >- in a future version of the standard. After upgrading to this version of Consul API Gateway, you should rename all - existing `ReferencePolicy`y to `ReferenceGrant`s. Please see the upgrading + existing `ReferencePolicy`y to `ReferenceGrant`s. Refer to the [Upgrades](/docs/api-gateway/upgrades) instructions for additional details. ## Supported Software From 32dfb1615d476c2a14f419bfe649bb7dc0d60bcd Mon Sep 17 00:00:00 2001 From: Jeff-Apple <79924108+Jeff-Apple@users.noreply.github.com> Date: Tue, 16 Aug 2022 10:48:13 -0700 Subject: [PATCH 284/339] Added Known Issues and other edits to Rel Notes --- .../consul-api-gateway/v0_4_x.mdx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx index 4f440dc249..b0c336af72 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_4_x.mdx @@ -25,12 +25,15 @@ description: >- API. Once an API reaches `v1beta1` status, future versions must comply with several backward compatibility requirements. -- **URL Path Prefix Rewrite** - This release introduces support for rewriting a URL's path prefix when routing - HTTP traffic. To use this functionality, add a `URLRewrite` filter to an - `HTTPRoute` configuration. This enables the gateway to rewrite the URL path in a client's HTTP request before sending the request to a service. For example, you could configure the gateway to change the path from `//store/checkout` to `//cart/checkout`. Refer to the [usage documentation](/docs/api-gateway/usage) for additional information. +- **URL Path Prefix Rewrite** This release introduces support for rewriting a + URL's path prefix when routing HTTP traffic. To use this functionality, add a + `URLRewrite` filter to an `HTTPRoute` configuration. This enables the gateway + to rewrite the URL path in a client's HTTP request before sending the request + to a service. For example, you could configure the gateway to change the path + from `//store/checkout` to `//cart/checkout`. Refer to the [usage + documentation](/docs/api-gateway/usage) for additional information. -## What Has Changed +## What has Changed - **Reference Policy Renamed to Reference Grant** In v0.5.0 of the Kubernetes Gateway API, `ReferencePolicy` has been renamed to `ReferenceGrant`. This @@ -38,13 +41,13 @@ description: >- in a future version of the standard. After upgrading to this version of Consul API Gateway, you should rename all - existing `ReferencePolicy`y to `ReferenceGrant`s. Refer to the [Upgrades](/docs/api-gateway/upgrades) + existing `ReferencePolicy` to `ReferenceGrant`. Refer to the [Upgrades](/docs/api-gateway/upgrades) instructions for additional details. ## Supported Software - Consul 1.11.2+ -- HashiCorp Consul Helm chart 0.47.0+ +- HashiCorp Consul Helm chart 0.47.1+ - Kubernetes 1.21+ - Kubernetes 1.24 is not supported at this time. - Kubectl 1.21+ @@ -59,6 +62,14 @@ Supported version of the [Gateway API](https://gateway-api.sigs.k8s.io/) spec: v For detailed information on upgrading, please refer to the [Upgrades page](/docs/api-gateway/upgrades) +## Known Issues +The following issues are know to exist in the v0.4.0 release + +- API Gateway pods fail to start if namespace mirroring enabled and destination + namespace doesn't exist. See GitHub Issue + [#248](https://github.com/hashicorp/consul-api-gateway/issues/248) for + details. + ## Changelogs The changelogs for this major release version and any maintenance versions are listed below. From 1c728ebcbb3f1e16fecf52689bd2e144d07729a1 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Tue, 16 Aug 2022 15:33:33 -0700 Subject: [PATCH 285/339] ci: Replace Nomad integration tests with predictable compatibility matrix (#14220) --- .circleci/config.yml | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index af1a2f5c66..60d6c34139 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,6 +28,10 @@ references: - "1.21.4" - "1.22.2" - "1.23.0" + nomad-versions: &supported_nomad_versions + - &default_nomad_version "1.3.3" + - "1.2.10" + - "1.1.16" images: # When updating the Go version, remember to also update the versions in the # workflows section for go-test-lib jobs. @@ -560,17 +564,20 @@ jobs: - run: make ci.dev-docker - run: *notify-slack-failure - # Nomad 0.8 builds on go1.10 - # Run integration tests on nomad/v0.8.7 - nomad-integration-0_8: + nomad-integration-test: &NOMAD_TESTS docker: - - image: docker.mirror.hashicorp.services/cimg/go:1.10 + - image: docker.mirror.hashicorp.services/cimg/go:1.19 + parameters: + nomad-version: + type: enum + enum: *supported_nomad_versions + default: *default_nomad_version environment: <<: *ENVIRONMENT NOMAD_WORKING_DIR: &NOMAD_WORKING_DIR /home/circleci/go/src/github.com/hashicorp/nomad - NOMAD_VERSION: v0.8.7 + NOMAD_VERSION: << parameters.nomad-version >> steps: &NOMAD_INTEGRATION_TEST_STEPS - - run: git clone https://github.com/hashicorp/nomad.git --branch ${NOMAD_VERSION} ${NOMAD_WORKING_DIR} + - run: git clone https://github.com/hashicorp/nomad.git --branch v${NOMAD_VERSION} ${NOMAD_WORKING_DIR} # get consul binary - attach_workspace: @@ -601,16 +608,6 @@ jobs: path: *TEST_RESULTS_DIR - run: *notify-slack-failure - # run integration tests on nomad/main - nomad-integration-main: - docker: - - image: docker.mirror.hashicorp.services/cimg/go:1.18 - environment: - <<: *ENVIRONMENT - NOMAD_WORKING_DIR: /home/circleci/go/src/github.com/hashicorp/nomad - NOMAD_VERSION: main - steps: *NOMAD_INTEGRATION_TEST_STEPS - # build frontend yarn cache frontend-cache: docker: @@ -1117,12 +1114,12 @@ workflows: - dev-upload-docker: <<: *dev-upload context: consul-ci - - nomad-integration-main: - requires: - - dev-build - - nomad-integration-0_8: + - nomad-integration-test: requires: - dev-build + matrix: + parameters: + nomad-version: *supported_nomad_versions - envoy-integration-test: requires: - dev-build From 6d1259ec8ff26210f4b2697c3e238bb8b038af76 Mon Sep 17 00:00:00 2001 From: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> Date: Tue, 16 Aug 2022 23:08:09 -0400 Subject: [PATCH 286/339] docs: fix broken markdown --- website/content/docs/agent/config/config-files.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/content/docs/agent/config/config-files.mdx b/website/content/docs/agent/config/config-files.mdx index 5c4f7b909d..da17549b0e 100644 --- a/website/content/docs/agent/config/config-files.mdx +++ b/website/content/docs/agent/config/config-files.mdx @@ -1998,7 +1998,7 @@ specially crafted certificate signed by the CA can be used to gain full access t Certificate Authority from the [`ca_file`](#tls_defaults_ca_file) or [`ca_path`](#tls_defaults_ca_path). By default, this is false, and Consul will not make use of TLS for outgoing connections. This applies to clients - and servers as both will make outgoing connections. This setting *does not* + and servers as both will make outgoing connections. This setting does not apply to the gRPC interface as Consul makes no outgoing connections on this interface. @@ -2071,7 +2071,9 @@ specially crafted certificate signed by the CA can be used to gain full access t set to true, Consul verifies the TLS certificate presented by the servers match the hostname `server..`. By default this is false, and Consul does not verify the hostname of the certificate, only that it - is signed by a trusted CA. This setting *must* be enabled to prevent a + is signed by a trusted CA. + + ~> **Security Note:** `verify_server_hostname` *must* be set to true to prevent a compromised client from gaining full read and write access to all cluster data *including all ACL tokens and Connect CA root keys*. From f92883bbced22166c512bb8db3095d9d20a258c1 Mon Sep 17 00:00:00 2001 From: James Hartig Date: Tue, 16 Aug 2022 16:54:01 -0400 Subject: [PATCH 287/339] Use the maximum jitter when calculating the timeout The timeout should include the maximum possible jitter since the server will randomly add to it's timeout a jitter. If the server's timeout is less than the client's timeout then the client will return an i/o deadline reached error. Before: ``` time curl 'http://localhost:8500/v1/catalog/service/service?dc=other-dc&stale=&wait=600s&index=15820644' rpc error making call: i/o deadline reached real 10m11.469s user 0m0.018s sys 0m0.023s ``` After: ``` time curl 'http://localhost:8500/v1/catalog/service/service?dc=other-dc&stale=&wait=600s&index=15820644' [...] real 10m35.835s user 0m0.021s sys 0m0.021s ``` --- .changelog/14233.txt | 3 +++ agent/consul/client_test.go | 7 ++++--- agent/structs/structs.go | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .changelog/14233.txt diff --git a/.changelog/14233.txt b/.changelog/14233.txt new file mode 100644 index 0000000000..5a2c6dee18 --- /dev/null +++ b/.changelog/14233.txt @@ -0,0 +1,3 @@ +```release-note:bugfix +rpc: Adds max jitter to client deadlines to prevent i/o deadline errors on blocking queries +``` diff --git a/agent/consul/client_test.go b/agent/consul/client_test.go index 84135ee184..32199d8aba 100644 --- a/agent/consul/client_test.go +++ b/agent/consul/client_test.go @@ -893,8 +893,8 @@ func TestClient_RPC_Timeout(t *testing.T) { } }) - // waiter will sleep for 50ms - require.NoError(t, s1.RegisterEndpoint("Wait", &waiter{duration: 50 * time.Millisecond})) + // waiter will sleep for 101ms which is 1ms more than the DefaultQueryTime + require.NoError(t, s1.RegisterEndpoint("Wait", &waiter{duration: 101 * time.Millisecond})) // Requests with QueryOptions have a default timeout of RPCHoldTimeout (10ms) // so we expect the RPC call to timeout. @@ -903,7 +903,8 @@ func TestClient_RPC_Timeout(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "rpc error making call: i/o deadline reached") - // Blocking requests have a longer timeout (100ms) so this should pass + // Blocking requests have a longer timeout (100ms) so this should pass since we + // add the maximum jitter which should be 16ms out = struct{}{} err = c1.RPC("Wait.Wait", &structs.NodeSpecificRequest{ QueryOptions: structs.QueryOptions{ diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 22fb47ca9d..8301688886 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -353,7 +353,7 @@ func (q QueryOptions) Timeout(rpcHoldTimeout, maxQueryTime, defaultQueryTime tim q.MaxQueryTime = defaultQueryTime } // Timeout after maximum jitter has elapsed. - q.MaxQueryTime += lib.RandomStagger(q.MaxQueryTime / JitterFraction) + q.MaxQueryTime += q.MaxQueryTime / JitterFraction return q.MaxQueryTime + rpcHoldTimeout } From 925eaf31feaab25ef3263f807b6bcc46c8cffe9b Mon Sep 17 00:00:00 2001 From: Michele Degges Date: Wed, 17 Aug 2022 14:48:43 -0700 Subject: [PATCH 288/339] set PRODUCT_VERSION for docker build (#14242) Changes proposed in this PR: In `actions-docker-build` we [pass](https://github.com/hashicorp/actions-docker-build/blob/05c370a26e61b06be46c5095d6e914c9f0ea4f3d/scripts/docker_build#L49) `PRODUCT_VERSION` to the docker build command. Since this was not set, the label did not populate properly which is used in a comparison to determine the `minor-latest` and `latest` docker image tags. How I've tested this PR: - build the image up to the point of label creation and pass in `--build-arg PRODUCT_VERSION=1.2.3` - inspect the image for the label with the above command How I expect reviewers to test this PR: - same as above Related [internal-only] post about this: https://hashicorp.atlassian.net/wiki/spaces/RELENG/pages/2416934922/August+17+2022-+Docker+Build+Failures --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 762471eb5a..f47ca41615 100644 --- a/Dockerfile +++ b/Dockerfile @@ -110,13 +110,13 @@ CMD ["agent", "-dev", "-client", "0.0.0.0"] # Remember, this image cannot be built locally. FROM docker.mirror.hashicorp.services/alpine:3.15 as default -ARG VERSION +ARG PRODUCT_VERSION ARG BIN_NAME # PRODUCT_NAME and PRODUCT_VERSION are the name of the software on releases.hashicorp.com # and the version to download. Example: PRODUCT_NAME=consul PRODUCT_VERSION=1.2.3. ENV BIN_NAME=$BIN_NAME -ENV VERSION=$VERSION +ENV VERSION=$PRODUCT_VERSION ARG PRODUCT_REVISION ARG PRODUCT_NAME=$BIN_NAME @@ -128,7 +128,7 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.url="https://www.consul.io/" \ org.opencontainers.image.documentation="https://www.consul.io/docs" \ org.opencontainers.image.source="https://github.com/hashicorp/consul" \ - org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.version=${PRODUCT_VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." @@ -217,7 +217,7 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.url="https://www.consul.io/" \ org.opencontainers.image.documentation="https://www.consul.io/docs" \ org.opencontainers.image.source="https://github.com/hashicorp/consul" \ - org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.version=${PRODUCT_VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." @@ -284,4 +284,4 @@ USER 100 # By default you'll get an insecure single-node development server that stores # everything in RAM, exposes a web UI and HTTP endpoints, and bootstraps itself. # Don't use this configuration for production. -CMD ["agent", "-dev", "-client", "0.0.0.0"] \ No newline at end of file +CMD ["agent", "-dev", "-client", "0.0.0.0"] From e84e4b886825001a8c3982d14fbeae26ef0b7686 Mon Sep 17 00:00:00 2001 From: cskh Date: Wed, 17 Aug 2022 21:14:04 -0400 Subject: [PATCH 289/339] Fix: upgrade pkg imdario/merg to prevent merge config panic (#14237) * upgrade imdario/merg to prevent merge config panic * test: service definition takes precedence over service-defaults in merged results --- agent/consul/merge_service_config_test.go | 10 ++++++++-- go.mod | 4 ++-- go.sum | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/agent/consul/merge_service_config_test.go b/agent/consul/merge_service_config_test.go index 5a866dce2a..dd9b1cbca8 100644 --- a/agent/consul/merge_service_config_test.go +++ b/agent/consul/merge_service_config_test.go @@ -153,6 +153,12 @@ func Test_MergeServiceConfig_UpstreamOverrides(t *testing.T) { DestinationNamespace: "default", DestinationPartition: "default", DestinationName: "zap", + Config: map[string]interface{}{ + "passive_health_check": map[string]interface{}{ + "Interval": int64(20), + "MaxFailures": int64(4), + }, + }, }, }, }, @@ -171,8 +177,8 @@ func Test_MergeServiceConfig_UpstreamOverrides(t *testing.T) { DestinationName: "zap", Config: map[string]interface{}{ "passive_health_check": map[string]interface{}{ - "Interval": int64(10), - "MaxFailures": int64(2), + "Interval": int64(20), + "MaxFailures": int64(4), }, "protocol": "grpc", }, diff --git a/go.mod b/go.mod index e2fbafed4b..1ade7d6de2 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 - github.com/imdario/mergo v0.3.6 + github.com/imdario/mergo v0.3.13 github.com/kr/text v0.2.0 github.com/miekg/dns v1.1.41 github.com/mitchellh/cli v1.1.0 @@ -183,7 +183,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/resty.v1 v1.12.0 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect k8s.io/klog v1.0.0 // indirect k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 // indirect sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index ee3e0beda1..cf7f2afc30 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= @@ -969,8 +970,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 72bae90724ad0211ae3f2f8e068550760b95b02b Mon Sep 17 00:00:00 2001 From: Mariano Asselborn Date: Thu, 18 Aug 2022 14:41:34 -0400 Subject: [PATCH 290/339] Add version label to Docker image (#14204) --- Dockerfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index f47ca41615..8e127254fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,11 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.url="https://www.consul.io/" \ org.opencontainers.image.documentation="https://www.consul.io/docs" \ org.opencontainers.image.source="https://github.com/hashicorp/consul" \ - org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.version=${VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ - org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." + org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." \ + version=${VERSION} # This is the location of the releases. ENV HASHICORP_RELEASES=https://releases.hashicorp.com @@ -116,7 +117,7 @@ ARG BIN_NAME # PRODUCT_NAME and PRODUCT_VERSION are the name of the software on releases.hashicorp.com # and the version to download. Example: PRODUCT_NAME=consul PRODUCT_VERSION=1.2.3. ENV BIN_NAME=$BIN_NAME -ENV VERSION=$PRODUCT_VERSION +ENV PRODUCT_VERSION=$PRODUCT_VERSION ARG PRODUCT_REVISION ARG PRODUCT_NAME=$BIN_NAME @@ -131,7 +132,8 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.version=${PRODUCT_VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ - org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." + org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." \ + version=${PRODUCT_VERSION} # Set up certificates and base tools. # libc6-compat is needed to symlink the shared libraries for ARM builds @@ -220,7 +222,8 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.version=${PRODUCT_VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ - org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." + org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." \ + version=${PRODUCT_VERSION} # Copy license for Red Hat certification. COPY LICENSE /licenses/mozilla.txt From 0e6c8401edfd51c6a1c839f192f810eca5747408 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Thu, 18 Aug 2022 12:59:03 -0700 Subject: [PATCH 291/339] Add missing changelog for 1.9.17 (#14053) --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b92ca84f31..f3399ba243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -944,6 +944,24 @@ NOTES: * legal: **(Enterprise only)** Enterprise binary downloads will now include a copy of the EULA and Terms of Evaluation in the zip archive +## 1.9.17 (April 13, 2022) + +SECURITY: + +* agent: Added a new check field, `disable_redirects`, that allows for disabling the following of redirects for HTTP checks. The intention is to default this to true in a future release so that redirects must explicitly be enabled. [[GH-12685](https://github.com/hashicorp/consul/issues/12685)] +* connect: Properly set SNI when configured for services behind a terminating gateway. [[GH-12672](https://github.com/hashicorp/consul/issues/12672)] + +DEPRECATIONS: + +* tls: With the upgrade to Go 1.17, the ordering of `tls_cipher_suites` will no longer be honored, and `tls_prefer_server_cipher_suites` is now ignored. [[GH-12767](https://github.com/hashicorp/consul/issues/12767)] + +BUG FIXES: + +* connect/ca: cancel old Vault renewal on CA configuration. Provide a 1 - 6 second backoff on repeated token renewal requests to prevent overwhelming Vault. [[GH-12607](https://github.com/hashicorp/consul/issues/12607)] +* memberlist: fixes a bug which prevented members from joining a cluster with +large amounts of churn [[GH-253](https://github.com/hashicorp/memberlist/issues/253)] [[GH-12046](https://github.com/hashicorp/consul/issues/12046)] +* replication: Fixed a bug which could prevent ACL replication from continuing successfully after a leader election. [[GH-12565](https://github.com/hashicorp/consul/issues/12565)] + ## 1.9.16 (February 28, 2022) FEATURES: From c201254ae9e2d111a2de2b4f9690651fa93b1c31 Mon Sep 17 00:00:00 2001 From: Jared Kirschner Date: Thu, 18 Aug 2022 09:26:46 -0700 Subject: [PATCH 292/339] docs: add 1.13 upgrade considerations Adds guidance when upgrading a Consul service mesh deployment to 1.13 and: - using auto-encrypt or auto-config; or - the HTTPS port is not enabled on Consul agents --- .../docs/agent/config/config-files.mdx | 2 +- .../docs/upgrading/instructions/index.mdx | 3 + .../docs/upgrading/upgrade-specific.mdx | 129 ++++++++++++++++-- 3 files changed, 122 insertions(+), 12 deletions(-) diff --git a/website/content/docs/agent/config/config-files.mdx b/website/content/docs/agent/config/config-files.mdx index 5c4f7b909d..3cd1428ac8 100644 --- a/website/content/docs/agent/config/config-files.mdx +++ b/website/content/docs/agent/config/config-files.mdx @@ -2082,7 +2082,7 @@ specially crafted certificate signed by the CA can be used to gain full access t ### Deprecated Options ((#tls_deprecated_options)) The following options were deprecated in Consul 1.12, please use the -[`tls`](#tls) stanza instead. +[`tls`](#tls-1) stanza instead. - `ca_file` See: [`tls.defaults.ca_file`](#tls_defaults_ca_file). diff --git a/website/content/docs/upgrading/instructions/index.mdx b/website/content/docs/upgrading/instructions/index.mdx index 466acf335f..4ddc86b07c 100644 --- a/website/content/docs/upgrading/instructions/index.mdx +++ b/website/content/docs/upgrading/instructions/index.mdx @@ -31,6 +31,9 @@ we recommend reviewing the changelog for versions between the one you are on and one you are upgrading to at each step to familiarize yourself with changes. Select your _currently installed_ release series: +- 1.12.x: work upwards from [1.13 upgrade notes](/docs/upgrading/upgrade-specific#consul-1-13-x) +- 1.11.x: work upwards from [1.12 upgrade notes](/docs/upgrading/upgrade-specific#consul-1-12-0) +- 1.10.x: work upwards from [1.11 upgrade notes](/docs/upgrading/upgrade-specific#consul-1-11-0) - [1.9.x](/docs/upgrading/instructions/upgrade-to-1-10-x) - [1.8.x](/docs/upgrading/instructions/upgrade-to-1-10-x) - [1.7.x](/docs/upgrading/instructions/upgrade-to-1-8-x) diff --git a/website/content/docs/upgrading/upgrade-specific.mdx b/website/content/docs/upgrading/upgrade-specific.mdx index 75884adb2e..2732ffe4f5 100644 --- a/website/content/docs/upgrading/upgrade-specific.mdx +++ b/website/content/docs/upgrading/upgrade-specific.mdx @@ -18,8 +18,15 @@ upgrade flow. ### Service Mesh Compatibility -Existing Consul deployments using service mesh (i.e., containing any registered -Connect proxies) should upgrade to **at least Consul 1.13.1**. +Before upgrading existing Consul deployments using service mesh to Consul 1.13.x, +review the following guidances relevant to your deployment: +- [All service mesh deployments](#all-service-mesh-deployments) +- [Service mesh deployments using auto-encrypt or auto-config](#service-mesh-deployments-using-auto-encrypt-or-auto-config) +- [Service mesh deployments without the HTTPS port enabled on Consul agents](#service-mesh-deployments-without-the-https-port-enabled-on-consul-agents) + +#### All service mesh deployments + +Upgrade to **Consul version 1.13.1 or later**. Consul 1.13.0 contains a bug that prevents Consul server agents from restoring saved state on startup if the state @@ -29,17 +36,117 @@ saved state on startup if the state This bug is fixed in Consul versions 1.13.1 and newer. -### gRPC TLS +#### Service mesh deployments using auto-encrypt or auto-config -In prior Consul versions if HTTPS was enabled for the client API and exposed -via `ports { https = NUMBER }` then the same TLS material was used to encrypt -the gRPC port used for xDS. Now this is decoupled and activating TLS on the -gRPC endpoint is controlled solely with the gRPC section of the new -[`tls` stanza](/docs/agent/config/config-files#tls-configuration-reference). +**Do not upgrade to Consul 1.13 yet** if using +[auto-encrypt](/docs/agent/config/config-files#auto_encrypt) or +[auto-config](/docs/agent/config/config-files#auto_config). -If you have not yet switched to the new `tls` stanza and were NOT using HTTPS -for the API then updating to Consul 1.13 will activate TLS for gRPC since the -deprecated TLS settings are used as defaults. +In Consul 1.13, auto-encrypt and auto-config both cause Consul +to require TLS for gRPC communication with Envoy proxies. +In environments where Envoy proxies are not already configured +to use TLS for gRPC, upgrading Consul 1.13 will cause +Envoy proxies to disconnect from the control plane (Consul agents). + +The underlying cause is the same as discussed in +[deployments without the HTTPS port enabled on Consul agents](#service-mesh-deployments-without-the-https-port-enabled-on-consul-agents). +However, when using auto-encrypt or auto-config, +the problem **cannot** currently be avoided by +[modifying the agent's TLS configuration](#modify-the-consul-agent-s-tls-configuration) +because auto-encrypt and auto-config automatically set +interface-generic TLS configuration in a manner similar to +[`tls.defaults`](/docs/agent/config/config-files#tls_defaults). +We are working to address this problem in an upcoming 1.13 patch release. + +#### Service mesh deployments without the HTTPS port enabled on Consul agents ((#grpc-tls)) + +If the HTTPS port is not enabled +([`ports { https = POSITIVE_INTEGER }`](/docs/agent/config/config-files#https_port)) +on a pre-1.13 Consul agent, +**[modify the agent's TLS configuration before upgrading](#modify-the-consul-agent-s-tls-configuration)** +to avoid Envoy proxies disconnecting from the control plane (Consul agents). +Envoy proxies include service mesh sidecars and gateways. + +##### Changes to gRPC and HTTP interface configuration + +If a Consul agent's HTTP API is exposed externally, +enabling HTTPS (TLS encryption for HTTP) is important. + +The gRPC interface is used for xDS communication between Consul and +Envoy proxies when using Consul service mesh. +A Consul agent's gRPC traffic is often loopback-only, +which TLS encryption is not important for. + +Prior to Consul 1.13, if [`ports { https = POSITIVE_INTEGER }`](/docs/agent/config/config-files#https_port) +was configured, TLS was enabled for both HTTP *and* gRPC. +This was inconvenient for deployments that +needed TLS for HTTP, but not for gRPC. +Enabling HTTPS also required launching Envoy proxies +with the necessary TLS material for xDS communication +with its Consul agent via TLS over gRPC. + +Consul 1.13 addresses this inconvenience by fully decoupling the TLS configuration for HTTP and gRPC interfaces. +TLS for gRPC is no longer enabled by setting +[`ports { https = POSITIVE_INTEGER }`](/docs/agent/config/config-files#https_port). +TLS configuration for gRPC is now determined exclusively by: + +1. [`tls.grpc`](/docs/agent/config/config-files#tls_grpc), which overrides +1. [`tls.defaults`](/docs/agent/config/config-files#tls_defaults), which overrides +1. [Deprecated TLS options](/docs/agent/config/config-files#tls_deprecated_options) such as + [`ca_file`](/docs/agent/config/config-files#ca_file-4), + [`cert_file`](/docs/agent/config/config-files#cert_file-4), and + [`key_file`](/docs/agent/config/config-files#key_file-4). + +This decoupling has a side effect that requires a +[TLS configuration change](#modify-the-consul-agent-s-tls-configuration) +for pre-1.13 agents without the HTTPS port enabled. +Without a TLS configuration change, +Consul 1.13 agents may now expect gRPC *with* TLS, +causing communication to fail with Envoy proxies +that continue to use gRPC *without* TLS. + +##### Modify the Consul agent's TLS configuration + +If [`tls.grpc`](/docs/agent/config/config-files#tls_grpc), +[`tls.defaults`](/docs/agent/config/config-files#tls_defaults), +or the [deprecated TLS options](/docs/agent/config/config-files#tls_deprecated_options) +define TLS material in their +`ca_file`, `ca_path`, `cert_file`, or `key_file` fields, +TLS for gRPC will be enabled in Consul 1.13, even if +[`ports { https = POSITIVE_INTEGER }`](/docs/agent/config/config-files#https_port) +is not set. + +This will cause Envoy proxies to disconnect from the control plane +after upgrading to Consul 1.13 if associated pre-1.13 Consul agents +have **not** set +[`ports { https = POSITIVE_INTEGER }`](/docs/agent/config/config-files#https_port). +To avoid this problem, make the following agent configuration changes: + +1. Remove TLS material from the Consul agents' + interface-generic TLS configuration options: + [`tls.defaults`](/docs/agent/config/config-files#tls_grpc) and + [deprecated TLS options](/docs/agent/config/config-files#tls_deprecated_options) +1. Reapply TLS material to the non-gRPC interfaces that need it with the + interface-specific TLS configuration stanzas + [introduced in Consul 1.12](/docs/upgrading/upgrade-specific#tls-configuration): + [`tls.https`](/docs/agent/config/config-files#tls_https) and + [`tls.internal_rpc`](/docs/agent/config/config-files#tls_internal_rpc). + + If upgrading directly from pre-1.12 Consul, + the above configuration change cannot be made before upgrading. + Therefore, consider upgrading agents to Consul 1.12 before upgrading to 1.13. + +If pre-1.13 Consul agents have set +[`ports { https = POSITIVE_INTEGER }`](/docs/agent/config/config-files#https_port), +this configuration change is not required to upgrade. +That setting means the pre-1.13 Consul agent requires TLS for gRPC *already*, +and will continue to do so after upgrading to 1.13. +If your pre-1.13 service mesh is working, you have already +configured your Envoy proxies to use TLS for gRPC when bootstrapping Envoy +via [`consul connect envoy`](/commands/connect/envoy), +such as with flags or environment variables like +[`-ca-file`](/commands/connect/envoy#ca-file) and +[`CONSUL_CACERT`](/commands#consul_cacert). ### 1.9 Telemetry Compatibility From 5384f5baad838aacaee8185c9f7400c5a6d098bb Mon Sep 17 00:00:00 2001 From: Chris Thain <32781396+cthain@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:06:20 -0700 Subject: [PATCH 293/339] Skip Lambda integration tests for fork PRs (#14257) --- .circleci/config.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 60d6c34139..105666c661 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -109,15 +109,18 @@ commands: type: env_var_name default: ROLE_ARN steps: + # Only run the assume-role command for the main repo. The AWS credentials aren't available for forks. - run: | - export AWS_ACCESS_KEY_ID="${<< parameters.access-key >>}" - export AWS_SECRET_ACCESS_KEY="${<< parameters.secret-key >>}" - export ROLE_ARN="${<< parameters.role-arn >>}" - # assume role has duration of 15 min (the minimum allowed) - CREDENTIALS="$(aws sts assume-role --duration-seconds 900 --role-arn ${ROLE_ARN} --role-session-name build-${CIRCLE_SHA1} | jq '.Credentials')" - echo "export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV - echo "export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV - echo "export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV + if [[ "${CIRCLE_BRANCH%%/*}/" != "pull/" ]]; then + export AWS_ACCESS_KEY_ID="${<< parameters.access-key >>}" + export AWS_SECRET_ACCESS_KEY="${<< parameters.secret-key >>}" + export ROLE_ARN="${<< parameters.role-arn >>}" + # assume role has duration of 15 min (the minimum allowed) + CREDENTIALS="$(aws sts assume-role --duration-seconds 900 --role-arn ${ROLE_ARN} --role-session-name build-${CIRCLE_SHA1} | jq '.Credentials')" + echo "export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV + echo "export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV + echo "export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV + fi run-go-test-full: parameters: From 527ebd068aaa410e7ddaccc6d9478fa22f31ff6c Mon Sep 17 00:00:00 2001 From: cskh Date: Fri, 19 Aug 2022 14:11:21 -0400 Subject: [PATCH 294/339] fix: missing MaxInboundConnections field in service-defaults config entry (#14072) * fix: missing max_inbound_connections field in merge config --- agent/consul/merge_service_config.go | 7 ++ agent/consul/merge_service_config_test.go | 47 +++++++++++ agent/structs/config_entry_test.go | 79 +++++++++++++++++++ api/config_entry.go | 31 ++++---- api/config_entry_test.go | 2 + .../config-entries/service-defaults.mdx | 6 ++ 6 files changed, 157 insertions(+), 15 deletions(-) diff --git a/agent/consul/merge_service_config.go b/agent/consul/merge_service_config.go index 027a2d3f5c..91fe229eae 100644 --- a/agent/consul/merge_service_config.go +++ b/agent/consul/merge_service_config.go @@ -159,6 +159,13 @@ func computeResolvedServiceConfig( thisReply.Destination = *serviceConf.Destination } + if serviceConf.MaxInboundConnections > 0 { + if thisReply.ProxyConfig == nil { + thisReply.ProxyConfig = map[string]interface{}{} + } + thisReply.ProxyConfig["max_inbound_connections"] = serviceConf.MaxInboundConnections + } + thisReply.Meta = serviceConf.Meta } diff --git a/agent/consul/merge_service_config_test.go b/agent/consul/merge_service_config_test.go index dd9b1cbca8..b34c85143e 100644 --- a/agent/consul/merge_service_config_test.go +++ b/agent/consul/merge_service_config_test.go @@ -3,12 +3,59 @@ package consul import ( "testing" + "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/structs" "github.com/mitchellh/copystructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func Test_ComputeResolvedServiceConfig(t *testing.T) { + type args struct { + scReq *structs.ServiceConfigRequest + upstreamIDs []structs.ServiceID + entries *configentry.ResolvedServiceConfigSet + } + + sid := structs.ServiceID{ + ID: "sid", + } + tests := []struct { + name string + args args + want *structs.ServiceConfigResponse + }{ + { + name: "proxy with maxinboundsconnections", + args: args{ + scReq: &structs.ServiceConfigRequest{ + Name: "sid", + }, + entries: &configentry.ResolvedServiceConfigSet{ + ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{ + sid: { + MaxInboundConnections: 20, + }, + }, + }, + }, + want: &structs.ServiceConfigResponse{ + ProxyConfig: map[string]interface{}{ + "max_inbound_connections": 20, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := computeResolvedServiceConfig(tt.args.scReq, tt.args.upstreamIDs, + false, tt.args.entries, nil) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + func Test_MergeServiceConfig_TransparentProxy(t *testing.T) { type args struct { defaults *structs.ServiceConfigResponse diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index c3f5c7a982..e462f6aa74 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -216,6 +216,85 @@ func testConfigEntries_ListRelatedServices_AndACLs(t *testing.T, cases []configE } } +func TestDecodeConfigEntry_ServiceDefaults(t *testing.T) { + + for _, tc := range []struct { + name string + camel string + snake string + expect ConfigEntry + expectErr string + }{ + { + name: "service-defaults-with-MaxInboundConnections", + snake: ` + kind = "service-defaults" + name = "external" + protocol = "tcp" + destination { + addresses = [ + "api.google.com", + "web.google.com" + ] + port = 8080 + } + max_inbound_connections = 14 + `, + camel: ` + Kind = "service-defaults" + Name = "external" + Protocol = "tcp" + Destination { + Addresses = [ + "api.google.com", + "web.google.com" + ] + Port = 8080 + } + MaxInboundConnections = 14 + `, + expect: &ServiceConfigEntry{ + Kind: "service-defaults", + Name: "external", + Protocol: "tcp", + Destination: &DestinationConfig{ + Addresses: []string{ + "api.google.com", + "web.google.com", + }, + Port: 8080, + }, + MaxInboundConnections: 14, + }, + }, + } { + tc := tc + + testbody := func(t *testing.T, body string) { + var raw map[string]interface{} + err := hcl.Decode(&raw, body) + require.NoError(t, err) + + got, err := DecodeConfigEntry(raw) + if tc.expectErr != "" { + require.Nil(t, got) + require.Error(t, err) + requireContainsLower(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + require.Equal(t, tc.expect, got) + } + } + + t.Run(tc.name+" (snake case)", func(t *testing.T) { + testbody(t, tc.snake) + }) + t.Run(tc.name+" (camel case)", func(t *testing.T) { + testbody(t, tc.camel) + }) + } +} + // TestDecodeConfigEntry is the 'structs' mirror image of // command/config/write/config_write_test.go:TestParseConfigEntry func TestDecodeConfigEntry(t *testing.T) { diff --git a/api/config_entry.go b/api/config_entry.go index ee55b55ad2..da685b7867 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -218,21 +218,22 @@ type UpstreamLimits struct { } type ServiceConfigEntry struct { - Kind string - Name string - Partition string `json:",omitempty"` - Namespace string `json:",omitempty"` - Protocol string `json:",omitempty"` - Mode ProxyMode `json:",omitempty"` - TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` - MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` - Expose ExposeConfig `json:",omitempty"` - ExternalSNI string `json:",omitempty" alias:"external_sni"` - UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"` - Destination *DestinationConfig `json:",omitempty"` - Meta map[string]string `json:",omitempty"` - CreateIndex uint64 - ModifyIndex uint64 + Kind string + Name string + Partition string `json:",omitempty"` + Namespace string `json:",omitempty"` + Protocol string `json:",omitempty"` + Mode ProxyMode `json:",omitempty"` + TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` + MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` + Expose ExposeConfig `json:",omitempty"` + ExternalSNI string `json:",omitempty" alias:"external_sni"` + UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"` + Destination *DestinationConfig `json:",omitempty"` + MaxInboundConnections int `json:",omitempty" alias:"max_inbound_connections"` + Meta map[string]string `json:",omitempty"` + CreateIndex uint64 + ModifyIndex uint64 } func (s *ServiceConfigEntry) GetKind() string { return s.Kind } diff --git a/api/config_entry_test.go b/api/config_entry_test.go index 4249e75471..63aba11b8a 100644 --- a/api/config_entry_test.go +++ b/api/config_entry_test.go @@ -104,6 +104,7 @@ func TestAPI_ConfigEntries(t *testing.T) { "foo": "bar", "gir": "zim", }, + MaxInboundConnections: 5, } dest := &DestinationConfig{ @@ -144,6 +145,7 @@ func TestAPI_ConfigEntries(t *testing.T) { require.Equal(t, service.Protocol, readService.Protocol) require.Equal(t, service.Meta, readService.Meta) require.Equal(t, service.Meta, readService.GetMeta()) + require.Equal(t, service.MaxInboundConnections, readService.MaxInboundConnections) // update it service.Protocol = "tcp" diff --git a/website/content/docs/connect/config-entries/service-defaults.mdx b/website/content/docs/connect/config-entries/service-defaults.mdx index 2dad3b5262..54aabfe8ef 100644 --- a/website/content/docs/connect/config-entries/service-defaults.mdx +++ b/website/content/docs/connect/config-entries/service-defaults.mdx @@ -687,6 +687,12 @@ represents a location outside the Consul cluster. They can be dialed directly wh }, ] }, + { + name: 'MaxInboundConnections', + description: 'The maximum number of concurrent inbound connections to each service instance.', + type: 'int: 0', + yaml: true, + }, { name: 'MeshGateway', type: 'MeshGatewayConfig: ', From acc5fd5c0ad01f5a54d079055e95c1ad355df51d Mon Sep 17 00:00:00 2001 From: Jared Kirschner Date: Fri, 19 Aug 2022 11:11:41 -0700 Subject: [PATCH 295/339] docs: add 1.13 upgrade considerations to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3399ba243..94217c74a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ connect: Terminating gateways with a wildcard service entry should no longer pic BREAKING CHANGES: * config-entry: Exporting a specific service name across all namespace is invalid. +* connect: contains an upgrade compatibility issue when restoring snapshots containing service mesh proxy registrations from pre-1.13 versions of Consul [[GH-14107](https://github.com/hashicorp/consul/issues/14107)]. Fixed in 1.13.1 [[GH-14149](https://github.com/hashicorp/consul/issues/14149)]. Refer to [1.13 upgrade guidance](https://www.consul.io/docs/upgrading/upgrade-specific#all-service-mesh-deployments) for more information. +* connect: if using auto-encrypt or auto-config, TLS is required for gRPC communication between Envoy and Consul as of 1.13.0; this TLS for gRPC requirement will be removed in a future 1.13 patch release. Refer to [1.13 upgrade guidance](https://www.consul.io/docs/upgrading/upgrade-specific#service-mesh-deployments-using-auto-encrypt-or-auto-config) for more information. +* connect: if a pre-1.13 Consul agent's HTTPS port was not enabled, upgrading to 1.13 may turn on TLS for gRPC communication for Envoy and Consul depending on the agent's TLS configuration. Refer to [1.13 upgrade guidance](https://www.consul.io/docs/upgrading/upgrade-specific#grpc-tls) for more information. * connect: Removes support for Envoy 1.19 [[GH-13807](https://github.com/hashicorp/consul/issues/13807)] * telemetry: config flag `telemetry { disable_compat_1.9 = (true|false) }` has been removed. Before upgrading you should remove this flag from your config if the flag is being used. [[GH-13532](https://github.com/hashicorp/consul/issues/13532)] From 9c72169d2684b8aca151e030415c2ae7ef4d2802 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 22 Aug 2022 12:33:42 -0400 Subject: [PATCH 296/339] Add example code for cross-namespace certificateRefs --- .../api-gateway/configuration/gateway.mdx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index a3f8594c65..652aa009f8 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -183,3 +183,49 @@ tls: ``` +#### Example cross-namespace certificateRef + +The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace`. This `Gateway` has a `certificateRef` in namespace `secret-namespace`. The reference is allowed because the `ReferenceGrant`, named `reference-grant` in namespace `secret-namespace`, allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace`. + + + + ```yaml + apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: example-gateway + namespace: gateway-namespace + spec: + gatewayClassName: consul-api-gateway + listeners: + - protocol: HTTPS + port: 443 + name: https + allowedRoutes: + namespaces: + from: Same + tls: + certificateRefs: + - name: cert + namespace: secret-namespace + group: "" + kind: Secret + --- + + apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: ReferenceGrant + metadata: + name: reference-grant + namespace: secret-namespace + spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: gateway-namespace + to: + - group: "" + kind: Secret + name: cert + ``` + + From 48e7af89b25f337bd6de53875cd0f7c5e1e92bbc Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 22 Aug 2022 12:34:16 -0400 Subject: [PATCH 297/339] Correct structure of existing tls.certificateRefs example --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 652aa009f8..7dbbb34fdd 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -174,7 +174,7 @@ In the following example, `tls` settings are configured to use a secret named `c tls: certificateRefs: - name: consul-server-cert + - name: consul-server-cert group: "" kind: Secret mode: Terminate From 060531a29a8b64ca603ca7a6790e86a494b91e84 Mon Sep 17 00:00:00 2001 From: cskh Date: Mon, 22 Aug 2022 13:51:04 -0400 Subject: [PATCH 298/339] Fix: add missing ent meta for test (#14289) --- agent/consul/merge_service_config_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/consul/merge_service_config_test.go b/agent/consul/merge_service_config_test.go index b34c85143e..a497579a76 100644 --- a/agent/consul/merge_service_config_test.go +++ b/agent/consul/merge_service_config_test.go @@ -18,7 +18,8 @@ func Test_ComputeResolvedServiceConfig(t *testing.T) { } sid := structs.ServiceID{ - ID: "sid", + ID: "sid", + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), } tests := []struct { name string From 60c82757ea35dc2b5dc5fa9c47441a0199c430ad Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Mon, 22 Aug 2022 11:04:51 -0700 Subject: [PATCH 299/339] Update requirements.mdx (#14286) * Update requirements.mdx --- website/content/docs/ecs/requirements.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/website/content/docs/ecs/requirements.mdx b/website/content/docs/ecs/requirements.mdx index e7c0a8ea87..0fb0ab451c 100644 --- a/website/content/docs/ecs/requirements.mdx +++ b/website/content/docs/ecs/requirements.mdx @@ -10,6 +10,7 @@ description: >- The following requirements must be met in order to install Consul on ECS: * **Launch Type:** Fargate and EC2 launch types are supported. +* **Network Mode:** Only `awsvpc` mode is supported. * **Subnets:** ECS Tasks can run in private or public subnets. Tasks must have [network access](https://aws.amazon.com/premiumsupport/knowledge-center/ecs-pull-container-api-error-ecr/) to Amazon ECR or other public container registries to pull images. * **Consul Servers:** You can use your own Consul servers running on virtual machines or use [HashiCorp Cloud Platform Consul](https://www.hashicorp.com/cloud-platform) to host the servers for you. For development purposes or testing, you may use the `dev-server` [Terraform module](https://github.com/hashicorp/terraform-aws-consul-ecs/tree/main) that runs the Consul server as an ECS task. The `dev-server` does not support persistent storage. * **ACL Controller:** If you are running a secure Consul installation with ACLs enabled, configure the ACL controller. From 022c15566052616f561b2b6d32fa3f78f47a36e3 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 22 Aug 2022 14:31:19 -0400 Subject: [PATCH 300/339] Apply suggestions from code review Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 7dbbb34fdd..89da4ba221 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -185,9 +185,9 @@ tls: #### Example cross-namespace certificateRef -The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace`. This `Gateway` has a `certificateRef` in namespace `secret-namespace`. The reference is allowed because the `ReferenceGrant`, named `reference-grant` in namespace `secret-namespace`, allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace`. +The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace` (lines 2-4). The gateway has a `certificateRef` in namespace `secret-namespace` (lines 16-18). The reference is allowed because the `ReferenceGrant` configuration, named `reference-grant` in namespace `secret-namespace` (lines 23-26), allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace` (lines 30-34). - + ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 From 226bfa8203d8d0c9b4a7e8f43cb29c601321fa07 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 22 Aug 2022 14:40:43 -0400 Subject: [PATCH 301/339] Update website/content/docs/api-gateway/configuration/gateway.mdx --- website/content/docs/api-gateway/configuration/gateway.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 89da4ba221..be99062921 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -187,7 +187,7 @@ tls: The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace` (lines 2-4). The gateway has a `certificateRef` in namespace `secret-namespace` (lines 16-18). The reference is allowed because the `ReferenceGrant` configuration, named `reference-grant` in namespace `secret-namespace` (lines 23-26), allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace` (lines 30-34). - + ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 From 596ab31262c917f020ee1ce25089e164cd29515a Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 22 Aug 2022 15:14:30 -0400 Subject: [PATCH 302/339] Apply suggestions from code review Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index be99062921..43fc270182 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -185,9 +185,9 @@ tls: #### Example cross-namespace certificateRef -The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace` (lines 2-4). The gateway has a `certificateRef` in namespace `secret-namespace` (lines 16-18). The reference is allowed because the `ReferenceGrant` configuration, named `reference-grant` in namespace `secret-namespace` (lines 23-26), allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace` (lines 30-34). +The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace` (lines 2-4). The gateway has a `certificateRef` in namespace `secret-namespace` (lines 16-18). The reference is allowed because the `ReferenceGrant` configuration, named `reference-grant` in namespace `secret-namespace` (lines 23-26), allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace` (lines 30-35). - + ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 From c1be820d85f5a94806082fa50fa5f3cc98c5b9a2 Mon Sep 17 00:00:00 2001 From: Nathan Coleman Date: Mon, 22 Aug 2022 16:22:43 -0400 Subject: [PATCH 303/339] Update website/content/docs/api-gateway/configuration/gateway.mdx Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- website/content/docs/api-gateway/configuration/gateway.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/api-gateway/configuration/gateway.mdx b/website/content/docs/api-gateway/configuration/gateway.mdx index 43fc270182..240b19721d 100644 --- a/website/content/docs/api-gateway/configuration/gateway.mdx +++ b/website/content/docs/api-gateway/configuration/gateway.mdx @@ -185,9 +185,9 @@ tls: #### Example cross-namespace certificateRef -The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace` (lines 2-4). The gateway has a `certificateRef` in namespace `secret-namespace` (lines 16-18). The reference is allowed because the `ReferenceGrant` configuration, named `reference-grant` in namespace `secret-namespace` (lines 23-26), allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace` (lines 30-35). +The following example creates a `Gateway` named `example-gateway` in namespace `gateway-namespace` (lines 2-4). The gateway has a `certificateRef` in namespace `secret-namespace` (lines 16-18). The reference is allowed because the `ReferenceGrant` configuration, named `reference-grant` in namespace `secret-namespace` (lines 24-27), allows `Gateways` in `gateway-namespace` to reference `Secrets` in `secret-namespace` (lines 31-35). - + ```yaml apiVersion: gateway.networking.k8s.io/v1beta1 From 4f920610bf5c210320a25b3113e4e9e484308afa Mon Sep 17 00:00:00 2001 From: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> Date: Tue, 16 Aug 2022 16:50:03 -0400 Subject: [PATCH 304/339] docs: update k8s vault connect ca config docs - Add namespace to additionalConfig example - Improve the link to additional configuration options available --- website/content/docs/k8s/helm.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/website/content/docs/k8s/helm.mdx b/website/content/docs/k8s/helm.mdx index 837a03f562..be0c340800 100644 --- a/website/content/docs/k8s/helm.mdx +++ b/website/content/docs/k8s/helm.mdx @@ -270,14 +270,14 @@ Use these links to navigate to a particular top-level stanza. - `authMethodPath` ((#v-global-secretsbackend-vault-connectca-authmethodpath)) (`string: kubernetes`) - The mount path of the Kubernetes auth method in Vault. - `rootPKIPath` ((#v-global-secretsbackend-vault-connectca-rootpkipath)) (`string: ""`) - The path to a PKI secrets engine for the root certificate. - Please see https://www.consul.io/docs/connect/ca/vault#rootpkipath. + For more details, [Vault Connect CA configuration](https://www.consul.io/docs/connect/ca/vault#rootpkipath). - `intermediatePKIPath` ((#v-global-secretsbackend-vault-connectca-intermediatepkipath)) (`string: ""`) - The path to a PKI secrets engine for the generated intermediate certificate. - Please see https://www.consul.io/docs/connect/ca/vault#intermediatepkipath. + For more details, [Vault Connect CA configuration](https://www.consul.io/docs/connect/ca/vault#intermediatepkipath). - `additionalConfig` ((#v-global-secretsbackend-vault-connectca-additionalconfig)) (`string: {}`) - Additional Connect CA configuration in JSON format. - Please see https://www.consul.io/docs/connect/ca/vault#common-ca-config-options - for additional configuration options. + Please refer to [Vault Connect CA configuration](https://www.consul.io/docs/connect/ca/vault#configuration) + for all configuration options available for that provider. Example: @@ -286,7 +286,8 @@ Use these links to navigate to a particular top-level stanza. { "connect": [{ "ca_config": [{ - "leaf_cert_ttl": "36h" + "leaf_cert_ttl": "36h", + "namespace": "my-vault-ns" }] }] } From b0ef7a667433f59d3e7d77775ed10af10b747a9d Mon Sep 17 00:00:00 2001 From: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> Date: Fri, 29 Jul 2022 18:04:05 -0400 Subject: [PATCH 305/339] docs: link pq docs to relevant DNS lookup section --- website/content/api-docs/query.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/api-docs/query.mdx b/website/content/api-docs/query.mdx index 54a148e9ad..4ad4e2e905 100644 --- a/website/content/api-docs/query.mdx +++ b/website/content/api-docs/query.mdx @@ -11,7 +11,7 @@ The `/query` endpoints create, update, destroy, and execute prepared queries. Prepared queries allow you to register a complex service query and then execute it later via its ID or name to get a set of healthy nodes that provide a given service. This is particularly useful in combination with Consul's -[DNS Interface](/docs/discovery/dns) as it allows for much richer queries than +[DNS Interface](/docs/discovery/dns#prepared-query-lookups) as it allows for much richer queries than would be possible given the limited entry points exposed by DNS. Check the [Geo Failover tutorial](https://learn.hashicorp.com/tutorials/consul/automate-geo-failover) for details and From 58901ad7df9b649c01eac59edec4bafb3056d48a Mon Sep 17 00:00:00 2001 From: Eric Haberkorn Date: Tue, 23 Aug 2022 09:13:43 -0400 Subject: [PATCH 306/339] Cluster peering failover disco chain changes (#14296) --- agent/connect/sni_test.go | 33 ++- agent/consul/discovery_chain_endpoint_test.go | 17 +- agent/consul/discoverychain/compile.go | 216 +++++++------- agent/consul/discoverychain/compile_test.go | 273 ++++++++++++++---- agent/consul/state/peering_test.go | 8 +- agent/discovery_chain_endpoint_test.go | 27 +- agent/proxycfg/naming.go | 33 ++- agent/proxycfg/naming_test.go | 7 + agent/structs/config_entry_discoverychain.go | 29 ++ agent/structs/discovery_chain.go | 56 +++- agent/xds/failover_math_test.go | 35 ++- 11 files changed, 527 insertions(+), 207 deletions(-) diff --git a/agent/connect/sni_test.go b/agent/connect/sni_test.go index 26fae1da72..59e9f41fcd 100644 --- a/agent/connect/sni_test.go +++ b/agent/connect/sni_test.go @@ -178,20 +178,43 @@ func TestQuerySNI(t *testing.T) { func TestTargetSNI(t *testing.T) { // empty namespace, empty subset require.Equal(t, "api.default.foo."+testTrustDomainSuffix1, - TargetSNI(structs.NewDiscoveryTarget("api", "", "", "default", "foo"), testTrustDomain1)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain1)) require.Equal(t, "api.default.foo."+testTrustDomainSuffix1, - TargetSNI(structs.NewDiscoveryTarget("api", "", "", "", "foo"), testTrustDomain1)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + Datacenter: "foo", + }), testTrustDomain1)) // set namespace, empty subset require.Equal(t, "api.neighbor.foo."+testTrustDomainSuffix2, - TargetSNI(structs.NewDiscoveryTarget("api", "", "neighbor", "default", "foo"), testTrustDomain2)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + Namespace: "neighbor", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain2)) // empty namespace, set subset require.Equal(t, "v2.api.default.foo."+testTrustDomainSuffix1, - TargetSNI(structs.NewDiscoveryTarget("api", "v2", "", "default", "foo"), testTrustDomain1)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + ServiceSubset: "v2", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain1)) // set namespace, set subset require.Equal(t, "canary.api.neighbor.foo."+testTrustDomainSuffix2, - TargetSNI(structs.NewDiscoveryTarget("api", "canary", "neighbor", "default", "foo"), testTrustDomain2)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + ServiceSubset: "canary", + Namespace: "neighbor", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain2)) } diff --git a/agent/consul/discovery_chain_endpoint_test.go b/agent/consul/discovery_chain_endpoint_test.go index 21c34aa864..c1ad0fef35 100644 --- a/agent/consul/discovery_chain_endpoint_test.go +++ b/agent/consul/discovery_chain_endpoint_test.go @@ -56,8 +56,17 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) { return &resp, nil } - newTarget := func(service, serviceSubset, namespace, partition, datacenter string) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter) + newTarget := func(opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + if opts.Namespace == "" { + opts.Namespace = "default" + } + if opts.Partition == "" { + opts.Partition = "default" + } + if opts.Datacenter == "" { + opts.Datacenter = "dc1" + } + t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, connect.TestClusterID+".consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default @@ -119,7 +128,7 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc1": newTarget("web", "", "default", "default", "dc1"), + "web.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "web"}), }, }, } @@ -245,7 +254,7 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) { }, Targets: map[string]*structs.DiscoveryTarget{ "web.default.default.dc1": targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc1"), + newTarget(structs.DiscoveryTargetOpts{Service: "web"}), 33*time.Second, ), }, diff --git a/agent/consul/discoverychain/compile.go b/agent/consul/discoverychain/compile.go index ed664878b4..3a9a1f0ed7 100644 --- a/agent/consul/discoverychain/compile.go +++ b/agent/consul/discoverychain/compile.go @@ -8,6 +8,7 @@ import ( "github.com/mitchellh/hashstructure" "github.com/mitchellh/mapstructure" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" @@ -576,7 +577,10 @@ func (c *compiler) assembleChain() error { if router == nil { // If no router is configured, move on down the line to the next hop of // the chain. - node, err := c.getSplitterOrResolverNode(c.newTarget(c.serviceName, "", "", "", "")) + node, err := c.getSplitterOrResolverNode(c.newTarget(structs.DiscoveryTargetOpts{ + Service: c.serviceName, + })) + if err != nil { return err } @@ -626,11 +630,20 @@ func (c *compiler) assembleChain() error { ) if dest.ServiceSubset == "" { node, err = c.getSplitterOrResolverNode( - c.newTarget(svc, "", destNamespace, destPartition, ""), - ) + c.newTarget(structs.DiscoveryTargetOpts{ + Service: svc, + Namespace: destNamespace, + Partition: destPartition, + }, + )) } else { node, err = c.getResolverNode( - c.newTarget(svc, dest.ServiceSubset, destNamespace, destPartition, ""), + c.newTarget(structs.DiscoveryTargetOpts{ + Service: svc, + ServiceSubset: dest.ServiceSubset, + Namespace: destNamespace, + Partition: destPartition, + }), false, ) } @@ -642,7 +655,12 @@ func (c *compiler) assembleChain() error { // If we have a router, we'll add a catch-all route at the end to send // unmatched traffic to the next hop in the chain. - defaultDestinationNode, err := c.getSplitterOrResolverNode(c.newTarget(router.Name, "", router.NamespaceOrDefault(), router.PartitionOrDefault(), "")) + opts := structs.DiscoveryTargetOpts{ + Service: router.Name, + Namespace: router.NamespaceOrDefault(), + Partition: router.PartitionOrDefault(), + } + defaultDestinationNode, err := c.getSplitterOrResolverNode(c.newTarget(opts)) if err != nil { return err } @@ -674,26 +692,36 @@ func newDefaultServiceRoute(serviceName, namespace, partition string) *structs.S } } -func (c *compiler) newTarget(service, serviceSubset, namespace, partition, datacenter string) *structs.DiscoveryTarget { - if service == "" { +func (c *compiler) newTarget(opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + if opts.Service == "" { panic("newTarget called with empty service which makes no sense") } - t := structs.NewDiscoveryTarget( - service, - serviceSubset, - defaultIfEmpty(namespace, c.evaluateInNamespace), - defaultIfEmpty(partition, c.evaluateInPartition), - defaultIfEmpty(datacenter, c.evaluateInDatacenter), - ) + if opts.Peer == "" { + opts.Datacenter = defaultIfEmpty(opts.Datacenter, c.evaluateInDatacenter) + opts.Namespace = defaultIfEmpty(opts.Namespace, c.evaluateInNamespace) + opts.Partition = defaultIfEmpty(opts.Partition, c.evaluateInPartition) + } else { + // Don't allow Peer and Datacenter. + opts.Datacenter = "" + // Peer and Partition cannot both be set. + opts.Partition = acl.PartitionOrDefault("") + // Default to "default" rather than c.evaluateInNamespace. + opts.Namespace = acl.PartitionOrDefault(opts.Namespace) + } - // Set default connect SNI. This will be overridden later if the service - // has an explicit SNI value configured in service-defaults. - t.SNI = connect.TargetSNI(t, c.evaluateInTrustDomain) + t := structs.NewDiscoveryTarget(opts) - // Use the same representation for the name. This will NOT be overridden - // later. - t.Name = t.SNI + // We don't have the peer's trust domain yet so we can't construct the SNI. + if opts.Peer == "" { + // Set default connect SNI. This will be overridden later if the service + // has an explicit SNI value configured in service-defaults. + t.SNI = connect.TargetSNI(t, c.evaluateInTrustDomain) + + // Use the same representation for the name. This will NOT be overridden + // later. + t.Name = t.SNI + } prev, ok := c.loadedTargets[t.ID] if ok { @@ -703,34 +731,30 @@ func (c *compiler) newTarget(service, serviceSubset, namespace, partition, datac return t } -func (c *compiler) rewriteTarget(t *structs.DiscoveryTarget, service, serviceSubset, partition, namespace, datacenter string) *structs.DiscoveryTarget { - var ( - service2 = t.Service - serviceSubset2 = t.ServiceSubset - partition2 = t.Partition - namespace2 = t.Namespace - datacenter2 = t.Datacenter - ) +func (c *compiler) rewriteTarget(t *structs.DiscoveryTarget, opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + mergedOpts := t.ToDiscoveryTargetOpts() - if service != "" && service != service2 { - service2 = service + if opts.Service != "" && opts.Service != mergedOpts.Service { + mergedOpts.Service = opts.Service // Reset the chosen subset if we reference a service other than our own. - serviceSubset2 = "" + mergedOpts.ServiceSubset = "" } - if serviceSubset != "" { - serviceSubset2 = serviceSubset + if opts.ServiceSubset != "" { + mergedOpts.ServiceSubset = opts.ServiceSubset } - if partition != "" { - partition2 = partition + if opts.Partition != "" { + mergedOpts.Partition = opts.Partition } - if namespace != "" { - namespace2 = namespace + // Only use explicit Namespace with Peer + if opts.Namespace != "" || opts.Peer != "" { + mergedOpts.Namespace = opts.Namespace } - if datacenter != "" { - datacenter2 = datacenter + if opts.Datacenter != "" { + mergedOpts.Datacenter = opts.Datacenter } + mergedOpts.Peer = opts.Peer - return c.newTarget(service2, serviceSubset2, namespace2, partition2, datacenter2) + return c.newTarget(mergedOpts) } func (c *compiler) getSplitterOrResolverNode(target *structs.DiscoveryTarget) (*structs.DiscoveryGraphNode, error) { @@ -803,10 +827,13 @@ func (c *compiler) getSplitterNode(sid structs.ServiceID) (*structs.DiscoveryGra // fall through to group-resolver } - node, err := c.getResolverNode( - c.newTarget(splitID.ID, split.ServiceSubset, splitID.NamespaceOrDefault(), splitID.PartitionOrDefault(), ""), - false, - ) + opts := structs.DiscoveryTargetOpts{ + Service: splitID.ID, + ServiceSubset: split.ServiceSubset, + Namespace: splitID.NamespaceOrDefault(), + Partition: splitID.PartitionOrDefault(), + } + node, err := c.getResolverNode(c.newTarget(opts), false) if err != nil { return nil, err } @@ -881,11 +908,7 @@ RESOLVE_AGAIN: redirectedTarget := c.rewriteTarget( target, - redirect.Service, - redirect.ServiceSubset, - redirect.Partition, - redirect.Namespace, - redirect.Datacenter, + redirect.ToDiscoveryTargetOpts(), ) if redirectedTarget.ID != target.ID { target = redirectedTarget @@ -895,14 +918,9 @@ RESOLVE_AGAIN: // Handle default subset. if target.ServiceSubset == "" && resolver.DefaultSubset != "" { - target = c.rewriteTarget( - target, - "", - resolver.DefaultSubset, - "", - "", - "", - ) + target = c.rewriteTarget(target, structs.DiscoveryTargetOpts{ + ServiceSubset: resolver.DefaultSubset, + }) goto RESOLVE_AGAIN } @@ -1027,56 +1045,54 @@ RESOLVE_AGAIN: failover, ok = f["*"] } - if ok { - // Determine which failover definitions apply. - var failoverTargets []*structs.DiscoveryTarget - if len(failover.Datacenters) > 0 { - for _, dc := range failover.Datacenters { - // Rewrite the target as per the failover policy. - failoverTarget := c.rewriteTarget( - target, - failover.Service, - failover.ServiceSubset, - target.Partition, - failover.Namespace, - dc, - ) - if failoverTarget.ID != target.ID { // don't failover to yourself - failoverTargets = append(failoverTargets, failoverTarget) - } - } - } else { + if !ok { + return node, nil + } + + // Determine which failover definitions apply. + var failoverTargets []*structs.DiscoveryTarget + if len(failover.Datacenters) > 0 { + opts := failover.ToDiscoveryTargetOpts() + for _, dc := range failover.Datacenters { // Rewrite the target as per the failover policy. - failoverTarget := c.rewriteTarget( - target, - failover.Service, - failover.ServiceSubset, - target.Partition, - failover.Namespace, - "", - ) + opts.Datacenter = dc + failoverTarget := c.rewriteTarget(target, opts) if failoverTarget.ID != target.ID { // don't failover to yourself failoverTargets = append(failoverTargets, failoverTarget) } } - - // If we filtered everything out then no point in having a failover. - if len(failoverTargets) > 0 { - df := &structs.DiscoveryFailover{} - node.Resolver.Failover = df - - // Take care of doing any redirects or configuration loading - // related to targets by cheating a bit and recursing into - // ourselves. - for _, target := range failoverTargets { - failoverResolveNode, err := c.getResolverNode(target, true) - if err != nil { - return nil, err - } - failoverTarget := failoverResolveNode.Resolver.Target - df.Targets = append(df.Targets, failoverTarget) + } else if len(failover.Targets) > 0 { + for _, t := range failover.Targets { + // Rewrite the target as per the failover policy. + failoverTarget := c.rewriteTarget(target, t.ToDiscoveryTargetOpts()) + if failoverTarget.ID != target.ID { // don't failover to yourself + failoverTargets = append(failoverTargets, failoverTarget) } } + } else { + // Rewrite the target as per the failover policy. + failoverTarget := c.rewriteTarget(target, failover.ToDiscoveryTargetOpts()) + if failoverTarget.ID != target.ID { // don't failover to yourself + failoverTargets = append(failoverTargets, failoverTarget) + } + } + + // If we filtered everything out then no point in having a failover. + if len(failoverTargets) > 0 { + df := &structs.DiscoveryFailover{} + node.Resolver.Failover = df + + // Take care of doing any redirects or configuration loading + // related to targets by cheating a bit and recursing into + // ourselves. + for _, target := range failoverTargets { + failoverResolveNode, err := c.getResolverNode(target, true) + if err != nil { + return nil, err + } + failoverTarget := failoverResolveNode.Resolver.Target + df.Targets = append(df.Targets, failoverTarget) + } } } diff --git a/agent/consul/discoverychain/compile_test.go b/agent/consul/discoverychain/compile_test.go index 221ac757f9..6505fdb9ea 100644 --- a/agent/consul/discoverychain/compile_test.go +++ b/agent/consul/discoverychain/compile_test.go @@ -46,6 +46,7 @@ func TestCompile(t *testing.T) { "service and subset failover": testcase_ServiceAndSubsetFailover(), "datacenter failover": testcase_DatacenterFailover(), "datacenter failover with mesh gateways": testcase_DatacenterFailover_WithMeshGateways(), + "target failover": testcase_Failover_Targets(), "noop split to resolver with default subset": testcase_NoopSplit_WithDefaultSubset(), "resolver with default subset": testcase_Resolve_WithDefaultSubset(), "default resolver with external sni": testcase_DefaultResolver_ExternalSNI(), @@ -182,7 +183,7 @@ func testcase_JustRouterWithDefaults() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -244,7 +245,7 @@ func testcase_JustRouterWithNoDestination() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -294,7 +295,7 @@ func testcase_RouterWithDefaults_NoSplit_WithResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc1", nil), + newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, @@ -361,7 +362,7 @@ func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -426,7 +427,10 @@ func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestC }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc1", + }, nil), }, } @@ -498,7 +502,7 @@ func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc1", nil), + newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, @@ -584,8 +588,11 @@ func testcase_RouteBypassesSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "bypass.other.default.default.dc1": newTarget("other", "bypass", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "bypass.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "other", + ServiceSubset: "bypass", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == bypass", } @@ -638,7 +645,7 @@ func testcase_NoopSplit_DefaultResolver() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -694,7 +701,7 @@ func testcase_NoopSplit_WithResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc1", nil), + newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, @@ -776,12 +783,19 @@ func testcase_SubsetSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), - "v1.main.default.default.dc1": newTarget("main", "v1", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v1", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 1", } @@ -855,8 +869,8 @@ func testcase_ServiceSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "foo.default.default.dc1": newTarget("foo", "", "default", "default", "dc1", nil), - "bar.default.default.dc1": newTarget("bar", "", "default", "default", "dc1", nil), + "foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil), + "bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil), }, } @@ -935,7 +949,10 @@ func testcase_SplitBypassesSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "bypassed.next.default.default.dc1": newTarget("next", "bypassed", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "bypassed.next.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "next", + ServiceSubset: "bypassed", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == bypass", } @@ -973,7 +990,7 @@ func testcase_ServiceRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "other.default.default.dc1": newTarget("other", "", "default", "default", "dc1", nil), + "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } @@ -1019,7 +1036,10 @@ func testcase_ServiceAndSubsetRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.other.default.default.dc1": newTarget("other", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "other", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } @@ -1055,7 +1075,10 @@ func testcase_DatacenterRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc9": newTarget("main", "", "default", "default", "dc9", nil), + "main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc9", + }, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1095,7 +1118,10 @@ func testcase_DatacenterRedirect_WithMeshGateways() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc9": newTarget("main", "", "default", "default", "dc9", func(t *structs.DiscoveryTarget) { + "main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc9", + }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } @@ -1134,8 +1160,8 @@ func testcase_ServiceFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "backup.default.default.dc1": newTarget("backup", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1177,8 +1203,8 @@ func testcase_ServiceFailoverThroughRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "actual.default.default.dc1": newTarget("actual", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "actual.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "actual"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1220,8 +1246,8 @@ func testcase_Resolver_CircularFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "backup.default.default.dc1": newTarget("backup", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1261,8 +1287,11 @@ func testcase_ServiceAndSubsetFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "backup.main.default.default.dc1": newTarget("main", "backup", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "backup.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "backup", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == backup", } @@ -1301,9 +1330,15 @@ func testcase_DatacenterFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "main.default.default.dc2": newTarget("main", "", "default", "default", "dc2", nil), - "main.default.default.dc4": newTarget("main", "", "default", "default", "dc4", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc2", + }, nil), + "main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc4", + }, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1350,17 +1385,105 @@ func testcase_DatacenterFailover_WithMeshGateways() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), - "main.default.default.dc2": newTarget("main", "", "default", "default", "dc2", func(t *structs.DiscoveryTarget) { + "main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc2", + }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), - "main.default.default.dc4": newTarget("main", "", "default", "default", "dc4", func(t *structs.DiscoveryTarget) { + "main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc4", + }, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + }, + } + return compileTestCase{entries: entries, expect: expect} +} + +func testcase_Failover_Targets() compileTestCase { + entries := newEntries() + + entries.AddProxyDefaults(&structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + MeshGateway: structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + }, + }) + + entries.AddResolvers( + &structs.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + Failover: map[string]structs.ServiceResolverFailover{ + "*": { + Targets: []structs.ServiceResolverFailoverTarget{ + {Datacenter: "dc3"}, + {Service: "new-main"}, + {Peer: "cluster-01"}, + }, + }, + }, + }, + ) + + expect := &structs.CompiledDiscoveryChain{ + Protocol: "tcp", + StartNode: "resolver:main.default.default.dc1", + Nodes: map[string]*structs.DiscoveryGraphNode{ + "resolver:main.default.default.dc1": { + Type: structs.DiscoveryGraphNodeTypeResolver, + Name: "main.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + ConnectTimeout: 5 * time.Second, + Target: "main.default.default.dc1", + Failover: &structs.DiscoveryFailover{ + Targets: []string{ + "main.default.default.dc3", + "new-main.default.default.dc1", + "main.default.default.external.cluster-01", + }, + }, + }, + }, + }, + Targets: map[string]*structs.DiscoveryTarget{ + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + "main.default.default.dc3": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc3", + }, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + "new-main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "new-main"}, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + "main.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Peer: "cluster-01", + }, func(t *structs.DiscoveryTarget) { + t.SNI = "" + t.Name = "" + t.Datacenter = "" t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } @@ -1422,7 +1545,10 @@ func testcase_NoopSplit_WithDefaultSubset() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } @@ -1452,7 +1578,7 @@ func testcase_DefaultResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1488,7 +1614,7 @@ func testcase_DefaultResolver_WithProxyDefaults() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } @@ -1530,7 +1656,7 @@ func testcase_ServiceMetaProjection() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -1588,7 +1714,7 @@ func testcase_ServiceMetaProjectionWithRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "other.default.default.dc1": newTarget("other", "", "default", "default", "dc1", nil), + "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } @@ -1623,7 +1749,7 @@ func testcase_RedirectToDefaultResolverIsNotDefaultChain() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "other.default.default.dc1": newTarget("other", "", "default", "default", "dc1", nil), + "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } @@ -1658,7 +1784,10 @@ func testcase_Resolve_WithDefaultSubset() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } @@ -1692,7 +1821,7 @@ func testcase_DefaultResolver_ExternalSNI() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.SNI = "main.some.other.service.mesh" t.External = true }), @@ -1857,11 +1986,17 @@ func testcase_MultiDatacenterCanary() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc2": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc2", nil), + newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc2", + }, nil), 33*time.Second, ), "main.default.default.dc3": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc3", nil), + newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc3", + }, nil), 33*time.Second, ), }, @@ -2155,27 +2290,42 @@ func testcase_AllBellsAndWhistles() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "prod.redirected.default.default.dc1": newTarget("redirected", "prod", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "prod.redirected.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "redirected", + ServiceSubset: "prod", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "ServiceMeta.env == prod", } }), - "v1.main.default.default.dc1": newTarget("main", "v1", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v1", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 1", } }), - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), - "v3.main.default.default.dc1": newTarget("main", "v3", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v3.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v3", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 3", } }), - "default-subset.main.default.default.dc1": newTarget("main", "default-subset", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "default-subset.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "default-subset", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{OnlyPassing: true} }), }, @@ -2379,7 +2529,7 @@ func testcase_ResolverProtocolOverride() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, @@ -2413,7 +2563,7 @@ func testcase_ResolverProtocolOverrideIgnored() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, @@ -2451,7 +2601,7 @@ func testcase_RouterIgnored_ResolverProtocolOverride() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, @@ -2685,9 +2835,9 @@ func testcase_LBSplitterAndResolver() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "foo.default.default.dc1": newTarget("foo", "", "default", "default", "dc1", nil), - "bar.default.default.dc1": newTarget("bar", "", "default", "default", "dc1", nil), - "baz.default.default.dc1": newTarget("baz", "", "default", "default", "dc1", nil), + "foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil), + "bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil), + "baz.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "baz"}, nil), }, } @@ -2743,7 +2893,7 @@ func testcase_LBResolver() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -2791,8 +2941,17 @@ func newEntries() *configentry.DiscoveryChainSet { } } -func newTarget(service, serviceSubset, namespace, partition, datacenter string, modFn func(t *structs.DiscoveryTarget)) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter) +func newTarget(opts structs.DiscoveryTargetOpts, modFn func(t *structs.DiscoveryTarget)) *structs.DiscoveryTarget { + if opts.Namespace == "" { + opts.Namespace = "default" + } + if opts.Partition == "" { + opts.Partition = "default" + } + if opts.Datacenter == "" { + opts.Datacenter = "dc1" + } + t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, "trustdomain.consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default diff --git a/agent/consul/state/peering_test.go b/agent/consul/state/peering_test.go index b48e4f80d9..bfce75295c 100644 --- a/agent/consul/state/peering_test.go +++ b/agent/consul/state/peering_test.go @@ -1461,7 +1461,13 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { } newTarget := func(service, serviceSubset, datacenter string) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, "default", "default", datacenter) + t := structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: service, + ServiceSubset: serviceSubset, + Partition: "default", + Namespace: "default", + Datacenter: datacenter, + }) t.SNI = connect.TargetSNI(t, connect.TestTrustDomain) t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default diff --git a/agent/discovery_chain_endpoint_test.go b/agent/discovery_chain_endpoint_test.go index 8b4a7e2723..42c0825916 100644 --- a/agent/discovery_chain_endpoint_test.go +++ b/agent/discovery_chain_endpoint_test.go @@ -27,8 +27,17 @@ func TestDiscoveryChainRead(t *testing.T) { defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") - newTarget := func(service, serviceSubset, namespace, partition, datacenter string) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter) + newTarget := func(opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + if opts.Namespace == "" { + opts.Namespace = "default" + } + if opts.Partition == "" { + opts.Partition = "default" + } + if opts.Datacenter == "" { + opts.Datacenter = "dc1" + } + t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, connect.TestClusterID+".consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default @@ -99,7 +108,7 @@ func TestDiscoveryChainRead(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc1": newTarget("web", "", "default", "default", "dc1"), + "web.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "web"}), }, } require.Equal(t, expect, value.Chain) @@ -144,7 +153,7 @@ func TestDiscoveryChainRead(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc2": newTarget("web", "", "default", "default", "dc2"), + "web.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{Service: "web", Datacenter: "dc2"}), }, } require.Equal(t, expect, value.Chain) @@ -198,7 +207,7 @@ func TestDiscoveryChainRead(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc1": newTarget("web", "", "default", "default", "dc1"), + "web.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "web"}), }, } require.Equal(t, expect, value.Chain) @@ -264,11 +273,11 @@ func TestDiscoveryChainRead(t *testing.T) { }, Targets: map[string]*structs.DiscoveryTarget{ "web.default.default.dc1": targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc1"), + newTarget(structs.DiscoveryTargetOpts{Service: "web"}), 33*time.Second, ), "web.default.default.dc2": targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc2"), + newTarget(structs.DiscoveryTargetOpts{Service: "web", Datacenter: "dc2"}), 33*time.Second, ), }, @@ -280,7 +289,7 @@ func TestDiscoveryChainRead(t *testing.T) { })) expectTarget_DC1 := targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc1"), + newTarget(structs.DiscoveryTargetOpts{Service: "web"}), 22*time.Second, ) expectTarget_DC1.MeshGateway = structs.MeshGatewayConfig{ @@ -288,7 +297,7 @@ func TestDiscoveryChainRead(t *testing.T) { } expectTarget_DC2 := targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc2"), + newTarget(structs.DiscoveryTargetOpts{Service: "web", Datacenter: "dc2"}), 22*time.Second, ) expectTarget_DC2.MeshGateway = structs.MeshGatewayConfig{ diff --git a/agent/proxycfg/naming.go b/agent/proxycfg/naming.go index 3bb0854b04..08ff216edf 100644 --- a/agent/proxycfg/naming.go +++ b/agent/proxycfg/naming.go @@ -63,22 +63,29 @@ func NewUpstreamIDFromServiceID(sid structs.ServiceID) UpstreamID { return id } -// TODO(peering): confirm we don't need peername here func NewUpstreamIDFromTargetID(tid string) UpstreamID { - // Drop the leading subset if one is present in the target ID. - separators := strings.Count(tid, ".") - if separators > 3 { - prefix := tid[:strings.Index(tid, ".")+1] - tid = strings.TrimPrefix(tid, prefix) + var id UpstreamID + split := strings.Split(tid, ".") + + switch { + case split[len(split)-2] == "external": + id = UpstreamID{ + Name: split[0], + EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(split[2], split[1]), + Peer: split[4], + } + case len(split) == 5: + // Drop the leading subset if one is present in the target ID. + split = split[1:] + fallthrough + default: + id = UpstreamID{ + Name: split[0], + EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(split[2], split[1]), + Datacenter: split[3], + } } - split := strings.SplitN(tid, ".", 4) - - id := UpstreamID{ - Name: split[0], - EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(split[2], split[1]), - Datacenter: split[3], - } id.normalize() return id } diff --git a/agent/proxycfg/naming_test.go b/agent/proxycfg/naming_test.go index 23ff241658..2c4f5173a8 100644 --- a/agent/proxycfg/naming_test.go +++ b/agent/proxycfg/naming_test.go @@ -35,6 +35,13 @@ func TestUpstreamIDFromTargetID(t *testing.T) { Datacenter: "dc2", }, }, + "peered": { + tid: "foo.default.default.external.cluster-01", + expect: UpstreamID{ + Name: "foo", + Peer: "cluster-01", + }, + }, } for name, tc := range cases { diff --git a/agent/structs/config_entry_discoverychain.go b/agent/structs/config_entry_discoverychain.go index 8bc0305b00..0ea2609551 100644 --- a/agent/structs/config_entry_discoverychain.go +++ b/agent/structs/config_entry_discoverychain.go @@ -1233,6 +1233,16 @@ type ServiceResolverRedirect struct { Datacenter string `json:",omitempty"` } +func (r *ServiceResolverRedirect) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: r.Service, + ServiceSubset: r.ServiceSubset, + Namespace: r.Namespace, + Partition: r.Partition, + Datacenter: r.Datacenter, + } +} + // There are some restrictions on what is allowed in here: // // - Service, ServiceSubset, Namespace, Datacenters, and Targets cannot all be @@ -1275,6 +1285,14 @@ type ServiceResolverFailover struct { Targets []ServiceResolverFailoverTarget `json:",omitempty"` } +func (t *ServiceResolverFailover) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: t.Service, + ServiceSubset: t.ServiceSubset, + Namespace: t.Namespace, + } +} + func (f *ServiceResolverFailover) isEmpty() bool { return f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && len(f.Datacenters) == 0 && len(f.Targets) == 0 } @@ -1299,6 +1317,17 @@ type ServiceResolverFailoverTarget struct { Peer string `json:",omitempty"` } +func (t *ServiceResolverFailoverTarget) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: t.Service, + ServiceSubset: t.ServiceSubset, + Namespace: t.Namespace, + Partition: t.Partition, + Datacenter: t.Datacenter, + Peer: t.Peer, + } +} + // LoadBalancer determines the load balancing policy and configuration for services // issuing requests to this upstream service. type LoadBalancer struct { diff --git a/agent/structs/discovery_chain.go b/agent/structs/discovery_chain.go index 2bbe88f9ed..ca64d070d6 100644 --- a/agent/structs/discovery_chain.go +++ b/agent/structs/discovery_chain.go @@ -56,7 +56,12 @@ type CompiledDiscoveryChain struct { // ID returns an ID that encodes the service, namespace, partition, and datacenter. // This ID allows us to compare a discovery chain target to the chain upstream itself. func (c *CompiledDiscoveryChain) ID() string { - return chainID("", c.ServiceName, c.Namespace, c.Partition, c.Datacenter) + return chainID(DiscoveryTargetOpts{ + Service: c.ServiceName, + Namespace: c.Namespace, + Partition: c.Partition, + Datacenter: c.Datacenter, + }) } func (c *CompiledDiscoveryChain) CompoundServiceName() ServiceName { @@ -185,6 +190,7 @@ type DiscoveryTarget struct { Namespace string `json:",omitempty"` Partition string `json:",omitempty"` Datacenter string `json:",omitempty"` + Peer string `json:",omitempty"` MeshGateway MeshGatewayConfig `json:",omitempty"` Subset ServiceResolverSubset `json:",omitempty"` @@ -240,28 +246,52 @@ func (t *DiscoveryTarget) UnmarshalJSON(data []byte) error { return nil } -func NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter string) *DiscoveryTarget { +type DiscoveryTargetOpts struct { + Service string + ServiceSubset string + Namespace string + Partition string + Datacenter string + Peer string +} + +func NewDiscoveryTarget(opts DiscoveryTargetOpts) *DiscoveryTarget { t := &DiscoveryTarget{ - Service: service, - ServiceSubset: serviceSubset, - Namespace: namespace, - Partition: partition, - Datacenter: datacenter, + Service: opts.Service, + ServiceSubset: opts.ServiceSubset, + Namespace: opts.Namespace, + Partition: opts.Partition, + Datacenter: opts.Datacenter, + Peer: opts.Peer, } t.setID() return t } -func chainID(subset, service, namespace, partition, dc string) string { - // NOTE: this format is similar to the SNI syntax for simplicity - if subset == "" { - return fmt.Sprintf("%s.%s.%s.%s", service, namespace, partition, dc) +func (t *DiscoveryTarget) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: t.Service, + ServiceSubset: t.ServiceSubset, + Namespace: t.Namespace, + Partition: t.Partition, + Datacenter: t.Datacenter, + Peer: t.Peer, } - return fmt.Sprintf("%s.%s.%s.%s.%s", subset, service, namespace, partition, dc) +} + +func chainID(opts DiscoveryTargetOpts) string { + // NOTE: this format is similar to the SNI syntax for simplicity + if opts.Peer != "" { + return fmt.Sprintf("%s.%s.default.external.%s", opts.Service, opts.Namespace, opts.Peer) + } + if opts.ServiceSubset == "" { + return fmt.Sprintf("%s.%s.%s.%s", opts.Service, opts.Namespace, opts.Partition, opts.Datacenter) + } + return fmt.Sprintf("%s.%s.%s.%s.%s", opts.ServiceSubset, opts.Service, opts.Namespace, opts.Partition, opts.Datacenter) } func (t *DiscoveryTarget) setID() { - t.ID = chainID(t.ServiceSubset, t.Service, t.Namespace, t.Partition, t.Datacenter) + t.ID = chainID(t.ToDiscoveryTargetOpts()) } func (t *DiscoveryTarget) String() string { diff --git a/agent/xds/failover_math_test.go b/agent/xds/failover_math_test.go index 29ac17ffe1..296d1cc77f 100644 --- a/agent/xds/failover_math_test.go +++ b/agent/xds/failover_math_test.go @@ -15,15 +15,40 @@ func TestFirstHealthyTarget(t *testing.T) { warning := proxycfg.TestUpstreamNodesInStatus(t, "warning") critical := proxycfg.TestUpstreamNodesInStatus(t, "critical") - warnOnlyPassingTarget := structs.NewDiscoveryTarget("all-warn", "", "default", "default", "dc1") + warnOnlyPassingTarget := structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-warn", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }) warnOnlyPassingTarget.Subset.OnlyPassing = true - failOnlyPassingTarget := structs.NewDiscoveryTarget("all-fail", "", "default", "default", "dc1") + failOnlyPassingTarget := structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-fail", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }) failOnlyPassingTarget.Subset.OnlyPassing = true targets := map[string]*structs.DiscoveryTarget{ - "all-ok.default.dc1": structs.NewDiscoveryTarget("all-ok", "", "default", "default", "dc1"), - "all-warn.default.dc1": structs.NewDiscoveryTarget("all-warn", "", "default", "default", "dc1"), - "all-fail.default.default.dc1": structs.NewDiscoveryTarget("all-fail", "", "default", "default", "dc1"), + "all-ok.default.dc1": structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-ok", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }), + "all-warn.default.dc1": structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-warn", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }), + "all-fail.default.default.dc1": structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-fail", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }), "all-warn-onlypassing.default.dc1": warnOnlyPassingTarget, "all-fail-onlypassing.default.dc1": failOnlyPassingTarget, } From 589e7cfab4e8b4e214d1199919025fdbf360b285 Mon Sep 17 00:00:00 2001 From: Jared Kirschner Date: Tue, 2 Aug 2022 06:30:06 -0700 Subject: [PATCH 307/339] docs: improve consistency of DNS lookup variables Previously, some variables were wrapped in < > while others were not, creating ambiguity in whether some labels were a string literal or a variable. Now, all variables are wrapped in < >. --- website/content/docs/discovery/dns.mdx | 69 +++++++++++++++----------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/website/content/docs/discovery/dns.mdx b/website/content/docs/discovery/dns.mdx index f08a43c6ed..b50e8deeeb 100644 --- a/website/content/docs/discovery/dns.mdx +++ b/website/content/docs/discovery/dns.mdx @@ -52,7 +52,7 @@ There are fundamentally two types of queries: node lookups and service lookups. A node lookup, a simple query for the address of a named node, looks like this: ```text -.node[.datacenter]. +.node[.]. ``` For example, if we have a `foo` node with default settings, we could @@ -79,16 +79,16 @@ $ dig @127.0.0.1 -p 8600 foo.node.consul ANY ;; WARNING: recursion requested but not available ;; QUESTION SECTION: -;foo.node.consul. IN ANY +;foo.node.consul. IN ANY ;; ANSWER SECTION: -foo.node.consul. 0 IN A 10.1.10.12 -foo.node.consul. 0 IN TXT "meta_key=meta_value" -foo.node.consul. 0 IN TXT "value only" +foo.node.consul. 0 IN A 10.1.10.12 +foo.node.consul. 0 IN TXT "meta_key=meta_value" +foo.node.consul. 0 IN TXT "value only" ;; AUTHORITY SECTION: -consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 +consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 ``` By default the TXT records value will match the node's metadata key-value @@ -121,7 +121,7 @@ it is recommended to use the HTTP API to retrieve the list of nodes. The format of a standard service lookup is: ```text -[tag.].service[.datacenter]. +[.].service[.]. ``` The `tag` is optional, and, as with node lookups, the `datacenter` is as @@ -157,26 +157,37 @@ $ dig @127.0.0.1 -p 8600 consul.service.consul SRV ;; WARNING: recursion requested but not available ;; QUESTION SECTION: -;consul.service.consul. IN SRV +;consul.service.consul. IN SRV ;; ANSWER SECTION: -consul.service.consul. 0 IN SRV 1 1 8300 foobar.node.dc1.consul. +consul.service.consul. 0 IN SRV 1 1 8300 foobar.node.dc1.consul. ;; ADDITIONAL SECTION: -foobar.node.dc1.consul. 0 IN A 10.1.10.12 +foobar.node.dc1.consul. 0 IN A 10.1.10.12 ``` ### RFC 2782 Lookup -The format for RFC 2782 SRV lookups is: +Valid formats for RFC 2782 SRV lookups depend on +whether you want to filter results based on a service tag: - _._[.service][.datacenter][.domain] +- No filtering on service tag -Per [RFC 2782](https://tools.ietf.org/html/rfc2782), SRV queries should use -underscores, `_`, as a prefix to the `service` and `protocol` values in a query to -prevent DNS collisions. The `protocol` value can be any of the tags for a -service. If the service has no tags, `tcp` should be used. If `tcp` -is specified as the protocol, the query will not perform any tag filtering. + ```text + _._tcp[.service][.]. + ``` + +- Filtering on service tag specified in the RFC 2782 protocol field + + ```text + _._[.service][.]. + ``` + +Per [RFC 2782](https://tools.ietf.org/html/rfc2782), SRV queries must +prepend an underscore (`_`) to the `service` and `protocol` values in a query to +prevent DNS collisions. +To perform no tag-based filtering, specify `tcp` in the RFC 2782 protocol field. +To filter results on a service tag, specify the tag in the RFC 2782 protocol field. Other than the query format and default `tcp` protocol/tag value, the behavior of the RFC style lookup is the same as the standard style of lookup. @@ -196,13 +207,13 @@ $ dig @127.0.0.1 -p 8600 _rabbitmq._amqp.service.consul SRV ;; WARNING: recursion requested but not available ;; QUESTION SECTION: -;_rabbitmq._amqp.service.consul. IN SRV +;_rabbitmq._amqp.service.consul. IN SRV ;; ANSWER SECTION: -_rabbitmq._amqp.service.consul. 0 IN SRV 1 1 5672 rabbitmq.node1.dc1.consul. +_rabbitmq._amqp.service.consul. 0 IN SRV 1 1 5672 rabbitmq.node1.dc1.consul. ;; ADDITIONAL SECTION: -rabbitmq.node1.dc1.consul. 0 IN A 10.1.11.20 +rabbitmq.node1.dc1.consul. 0 IN A 10.1.11.20 ``` Again, note that the SRV record returns the port of the service as well as its IP. @@ -328,7 +339,7 @@ $ echo -n "20010db800010002cafe000000001337" | perl -ne 'printf join(":", unpack The format of a prepared query lookup is: ```text -.query[.datacenter]. +.query[.]. ``` The `datacenter` is optional, and if not provided, the datacenter of this Consul @@ -376,7 +387,7 @@ If you need more complex behavior, please use the To find the unique virtual IP allocated for a service: ```text -.virtual[.peer]. +.virtual[.]. ``` This will return the unique virtual IP for any [Connect-capable](/docs/connect) @@ -439,14 +450,14 @@ The following responses are returned: ``` ;; QUESTION SECTION: -;consul.service.test-domain. IN SRV +;consul.service.test-domain. IN SRV ;; ANSWER SECTION: -consul.service.test-domain. 0 IN SRV 1 1 8300 machine.node.dc1.test-domain. +consul.service.test-domain. 0 IN SRV 1 1 8300 machine.node.dc1.test-domain. ;; ADDITIONAL SECTION: -machine.node.dc1.test-domain. 0 IN A 127.0.0.1 -machine.node.dc1.test-domain. 0 IN TXT "consul-network-segment=" +machine.node.dc1.test-domain. 0 IN A 127.0.0.1 +machine.node.dc1.test-domain. 0 IN TXT "consul-network-segment=" ``` -> **PTR queries:** Responses to PTR queries (`.in-addr.arpa.`) will always use the @@ -479,7 +490,7 @@ resolve services within the `default` namespace and partition. However, for reso services from other namespaces or partitions the following form can be used: ```text -[tag.].service..ns..ap..dc. +[.].service..ns..ap..dc. ``` This sequence is the canonical naming convention of a Consul Enterprise service. At least two of the following @@ -491,14 +502,14 @@ fields must be present: For imported lookups, only the namespace and peer need to be specified as the partition can be inferred from the peering: ```text -.virtual[.namespace][.peer]. +.virtual[.].. ``` For node lookups, only the partition and datacenter need to be specified as nodes cannot be namespaced. ```text -[tag.].node..ap..dc. +[.].node..ap..dc. ``` ## DNS with ACLs From cb1043d8ace7edfa26e8703e79903753f7370ad4 Mon Sep 17 00:00:00 2001 From: Tyler Wendlandt Date: Tue, 23 Aug 2022 13:02:40 -0600 Subject: [PATCH 308/339] ui: Update badge / pill icon sizing (#14282) * Update badge icon sizing to be 16x16 * Update icon sizing in pill component --- .../app/components/consul/external-source/index.scss | 9 +++++++-- .../consul-ui/app/components/consul/kind/index.scss | 1 + ui/packages/consul-ui/app/components/pill/index.scss | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/packages/consul-ui/app/components/consul/external-source/index.scss b/ui/packages/consul-ui/app/components/consul/external-source/index.scss index b05acb45b2..b876b48fd4 100644 --- a/ui/packages/consul-ui/app/components/consul/external-source/index.scss +++ b/ui/packages/consul-ui/app/components/consul/external-source/index.scss @@ -1,6 +1,11 @@ .consul-external-source { @extend %pill-200, %frame-gray-600, %p1; } + +.consul-external-source::before { + --icon-size: icon-300; +} + .consul-external-source.kubernetes::before { @extend %with-logo-kubernetes-color-icon, %as-pseudo; } @@ -15,10 +20,10 @@ @extend %with-logo-consul-color-icon, %as-pseudo; } .consul-external-source.vault::before { - @extend %with-vault-100; + @extend %with-vault-300; } .consul-external-source.aws::before { - @extend %with-aws-100; + @extend %with-aws-300; } .consul-external-source.leader::before { @extend %with-star-outline-mask, %as-pseudo; diff --git a/ui/packages/consul-ui/app/components/consul/kind/index.scss b/ui/packages/consul-ui/app/components/consul/kind/index.scss index 7467195f2c..0431ac3068 100644 --- a/ui/packages/consul-ui/app/components/consul/kind/index.scss +++ b/ui/packages/consul-ui/app/components/consul/kind/index.scss @@ -3,4 +3,5 @@ } .consul-kind::before { @extend %with-gateway-mask, %as-pseudo; + --icon-size: icon-300; } diff --git a/ui/packages/consul-ui/app/components/pill/index.scss b/ui/packages/consul-ui/app/components/pill/index.scss index d08626db8b..c528bd9ff3 100644 --- a/ui/packages/consul-ui/app/components/pill/index.scss +++ b/ui/packages/consul-ui/app/components/pill/index.scss @@ -18,6 +18,9 @@ span.policy-node-identity::before { span.policy-service-identity::before { content: 'Service Identity: '; } +%pill::before { + --icon-size: icon-300; +} %pill.leader::before { @extend %with-star-outline-mask, %as-pseudo; } From 24a3975494f98050dd09285103420dd7f3789e5a Mon Sep 17 00:00:00 2001 From: Ashwin Venkatesh Date: Tue, 23 Aug 2022 15:14:36 -0400 Subject: [PATCH 309/339] Updates docs for CRDs (#14267) Co-authored-by: NicoletaPopoviciu --- .../docs/connect/config-entries/ingress-gateway.mdx | 6 ------ website/content/docs/connect/config-entries/mesh.mdx | 11 +---------- .../docs/connect/config-entries/proxy-defaults.mdx | 8 +++----- .../docs/connect/config-entries/service-defaults.mdx | 2 -- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/website/content/docs/connect/config-entries/ingress-gateway.mdx b/website/content/docs/connect/config-entries/ingress-gateway.mdx index 78773188de..fa95c5b197 100644 --- a/website/content/docs/connect/config-entries/ingress-gateway.mdx +++ b/website/content/docs/connect/config-entries/ingress-gateway.mdx @@ -991,14 +991,12 @@ You can specify the following parameters to configure ingress gateway configurat }, { name: 'TLSMinVersion', - yaml: false, type: 'string: ""', description: "Set the default minimum TLS version supported for the gateway's listeners. One of `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, or `TLSv1_3`. If unspecified, Envoy v1.22.0 and newer [will default to TLS 1.2 as a min version](https://github.com/envoyproxy/envoy/pull/19330), while older releases of Envoy default to TLS 1.0.", }, { name: 'TLSMaxVersion', - yaml: false, type: 'string: ""', description: { hcl: @@ -1009,7 +1007,6 @@ You can specify the following parameters to configure ingress gateway configurat }, { name: 'CipherSuites', - yaml: false, type: 'array: ', description: `Set the default list of TLS cipher suites for the gateway's listeners to support when negotiating connections using @@ -1179,21 +1176,18 @@ You can specify the following parameters to configure ingress gateway configurat }, { name: 'TLSMinVersion', - yaml: false, type: 'string: ""', description: 'Set the minimum TLS version supported for this listener. One of `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, or `TLSv1_3`. If unspecified, Envoy v1.22.0 and newer [will default to TLS 1.2 as a min version](https://github.com/envoyproxy/envoy/pull/19330), while older releases of Envoy default to TLS 1.0.', }, { name: 'TLSMaxVersion', - yaml: false, type: 'string: ""', description: 'Set the maximum TLS version supported for this listener. Must be greater than or equal to `TLSMinVersion`. One of `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, or `TLSv1_3`.', }, { name: 'CipherSuites', - yaml: false, type: 'array: ', description: `Set the list of TLS cipher suites to support when negotiating connections using TLS 1.2 or earlier. If unspecified, diff --git a/website/content/docs/connect/config-entries/mesh.mdx b/website/content/docs/connect/config-entries/mesh.mdx index 8c9f3e718e..e8d6b4de5f 100644 --- a/website/content/docs/connect/config-entries/mesh.mdx +++ b/website/content/docs/connect/config-entries/mesh.mdx @@ -271,7 +271,6 @@ Note that the Kubernetes example does not include a `partition` field. Configura children: [ { name: 'Incoming', - yaml: false, type: 'TLSDirectionConfig: ', description: `TLS configuration for inbound mTLS connections targeting the public listener on \`connect-proxy\` and \`terminating-gateway\` @@ -279,14 +278,12 @@ Note that the Kubernetes example does not include a `partition` field. Configura children: [ { name: 'TLSMinVersion', - yaml: false, type: 'string: ""', description: "Set the default minimum TLS version supported. One of `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, or `TLSv1_3`. If unspecified, Envoy v1.22.0 and newer [will default to TLS 1.2 as a min version](https://github.com/envoyproxy/envoy/pull/19330), while older releases of Envoy default to TLS 1.0.", }, { name: 'TLSMaxVersion', - yaml: false, type: 'string: ""', description: { hcl: @@ -297,7 +294,6 @@ Note that the Kubernetes example does not include a `partition` field. Configura }, { name: 'CipherSuites', - yaml: false, type: 'array: ', description: `Set the default list of TLS cipher suites to support when negotiating connections using @@ -315,7 +311,6 @@ Note that the Kubernetes example does not include a `partition` field. Configura }, { name: 'Outgoing', - yaml: false, type: 'TLSDirectionConfig: ', description: `TLS configuration for outbound mTLS connections dialing upstreams from \`connect-proxy\` and \`ingress-gateway\` @@ -323,14 +318,12 @@ Note that the Kubernetes example does not include a `partition` field. Configura children: [ { name: 'TLSMinVersion', - yaml: false, type: 'string: ""', description: "Set the default minimum TLS version supported. One of `TLS_AUTO`, `TLSv1_0`, `TLSv1_1`, `TLSv1_2`, or `TLSv1_3`. If unspecified, Envoy v1.22.0 and newer [will default to TLS 1.2 as a min version](https://github.com/envoyproxy/envoy/pull/19330), while older releases of Envoy default to TLS 1.0.", }, { name: 'TLSMaxVersion', - yaml: false, type: 'string: ""', description: { hcl: @@ -341,7 +334,6 @@ Note that the Kubernetes example does not include a `partition` field. Configura }, { name: 'CipherSuites', - yaml: false, type: 'array: ', description: `Set the default list of TLS cipher suites to support when negotiating connections using @@ -366,9 +358,8 @@ Note that the Kubernetes example does not include a `partition` field. Configura children: [ { name: 'SanitizeXForwardedClientCert', - yaml: false, type: 'bool: ', - description: `If configured to \`true\`, the \`forward_client_cert_details\` option will be set to \`SANITIZE\` + description: `If configured to \`true\`, the \`forward_client_cert_details\` option will be set to \`SANITIZE\` for all Envoy proxies. As a result, Consul will not include the \`x-forwarded-client-cert\` header in the next hop. If set to \`false\` (default), the XFCC header is propagated to upstream applications.`, }, diff --git a/website/content/docs/connect/config-entries/proxy-defaults.mdx b/website/content/docs/connect/config-entries/proxy-defaults.mdx index 3be5c850b1..c6f82d7835 100644 --- a/website/content/docs/connect/config-entries/proxy-defaults.mdx +++ b/website/content/docs/connect/config-entries/proxy-defaults.mdx @@ -10,7 +10,7 @@ description: >- # Proxy Defaults -The `proxy-defaults` configuration entry (`ProxyDefaults` on Kubernetes) allows you +The `proxy-defaults` configuration entry (`ProxyDefaults` on Kubernetes) allows you to configure global defaults across all services for Connect proxy configurations. Only one global entry is supported. @@ -28,8 +28,8 @@ service definitions](/docs/connect/registration/sidecar-service). ## Requirements The following Consul binaries are supported: -* Consul 1.8.4+ on Kubernetes. -* Consul 1.5.0+ on other platforms. +* Consul 1.8.4+ on Kubernetes. +* Consul 1.5.0+ on other platforms. ## Usage @@ -321,7 +321,6 @@ spec: \`direct\` represents that the proxy's listeners must be dialed directly by the local application and other proxies. Added in v1.10.0.`, - yaml: false, }, { name: 'TransparentProxy', @@ -333,7 +332,6 @@ spec: type: 'int: "15001"', description: `The port the proxy should listen on for outbound traffic. This must be the port where outbound application traffic is captured and redirected to.`, - yaml: false, }, { name: 'DialedDirectly', diff --git a/website/content/docs/connect/config-entries/service-defaults.mdx b/website/content/docs/connect/config-entries/service-defaults.mdx index 54aabfe8ef..b431e43459 100644 --- a/website/content/docs/connect/config-entries/service-defaults.mdx +++ b/website/content/docs/connect/config-entries/service-defaults.mdx @@ -366,7 +366,6 @@ represents a location outside the Consul cluster. They can be dialed directly wh \`direct\` represents that the proxy's listeners must be dialed directly by the local application and other proxies. Added in v1.10.0.`, - yaml: false, }, { name: 'UpstreamConfig', @@ -652,7 +651,6 @@ represents a location outside the Consul cluster. They can be dialed directly wh type: 'int: "15001"', description: `The port the proxy should listen on for outbound traffic. This must be the port where outbound application traffic is redirected to.`, - yaml: false, }, { name: 'DialedDirectly', From 13c04a13af70bcd8c00fcf55003212f584a92799 Mon Sep 17 00:00:00 2001 From: Daniel Upton Date: Thu, 11 Aug 2022 10:19:36 +0100 Subject: [PATCH 310/339] proxycfg: terminate stream on irrecoverable errors This is the OSS portion of enterprise PR 2339. It improves our handling of "irrecoverable" errors in proxycfg data sources. The canonical example of this is what happens when the ACL token presented by Envoy is deleted/revoked. Previously, the stream would get "stuck" until the xDS server re-checked the token (after 5 minutes) and terminated the stream. Materializers would also sit burning resources retrying something that could never succeed. Now, it is possible for data sources to mark errors as "terminal" which causes the xDS stream to be closed immediately. Similarly, the submatview.Store will evict materializers when it observes they have encountered such an error. --- agent/proxycfg-glue/glue.go | 20 +++--- agent/proxycfg-glue/intention_upstreams.go | 7 +-- agent/proxycfg-glue/intentions.go | 17 ++--- agent/proxycfg/data_sources.go | 23 +++++++ agent/proxycfg/manager.go | 39 +++++++++--- agent/proxycfg/state.go | 24 +++++++- agent/submatview/local_materializer.go | 12 ++++ agent/submatview/store.go | 60 +++++++++++++++++- agent/submatview/store_test.go | 72 ++++++++++++++++++++++ agent/xds/delta.go | 18 +++++- agent/xds/server.go | 22 ++++--- 11 files changed, 267 insertions(+), 47 deletions(-) diff --git a/agent/proxycfg-glue/glue.go b/agent/proxycfg-glue/glue.go index 86badf67e4..1b22b02bd4 100644 --- a/agent/proxycfg-glue/glue.go +++ b/agent/proxycfg-glue/glue.go @@ -124,15 +124,21 @@ func (c *cacheProxyDataSource[ReqType]) Notify( func dispatchCacheUpdate(ch chan<- proxycfg.UpdateEvent) cache.Callback { return func(ctx context.Context, e cache.UpdateEvent) { - u := proxycfg.UpdateEvent{ - CorrelationID: e.CorrelationID, - Result: e.Result, - Err: e.Err, - } - select { - case ch <- u: + case ch <- newUpdateEvent(e.CorrelationID, e.Result, e.Err): case <-ctx.Done(): } } } + +func newUpdateEvent(correlationID string, result any, err error) proxycfg.UpdateEvent { + // This roughly matches the logic in agent/submatview.LocalMaterializer.isTerminalError. + if acl.IsErrNotFound(err) { + err = proxycfg.TerminalError(err) + } + return proxycfg.UpdateEvent{ + CorrelationID: correlationID, + Result: result, + Err: err, + } +} diff --git a/agent/proxycfg-glue/intention_upstreams.go b/agent/proxycfg-glue/intention_upstreams.go index 186d91b357..a694d033b4 100644 --- a/agent/proxycfg-glue/intention_upstreams.go +++ b/agent/proxycfg-glue/intention_upstreams.go @@ -54,13 +54,8 @@ func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.Servi func dispatchBlockingQueryUpdate[ResultType any](ch chan<- proxycfg.UpdateEvent) func(context.Context, string, ResultType, error) { return func(ctx context.Context, correlationID string, result ResultType, err error) { - event := proxycfg.UpdateEvent{ - CorrelationID: correlationID, - Result: result, - Err: err, - } select { - case ch <- event: + case ch <- newUpdateEvent(correlationID, result, err): case <-ctx.Done(): } } diff --git a/agent/proxycfg-glue/intentions.go b/agent/proxycfg-glue/intentions.go index 57f48bdae9..69652d922d 100644 --- a/agent/proxycfg-glue/intentions.go +++ b/agent/proxycfg-glue/intentions.go @@ -39,12 +39,8 @@ func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecifi QueryOptions: structs.QueryOptions{Token: req.QueryOptions.Token}, } return c.c.NotifyCallback(ctx, cachetype.IntentionMatchName, query, correlationID, func(ctx context.Context, event cache.UpdateEvent) { - e := proxycfg.UpdateEvent{ - CorrelationID: correlationID, - Err: event.Err, - } - - if e.Err == nil { + var result any + if event.Err == nil { rsp, ok := event.Result.(*structs.IndexedIntentionMatches) if !ok { return @@ -54,11 +50,11 @@ func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecifi if len(rsp.Matches) != 0 { matches = rsp.Matches[0] } - e.Result = matches + result = matches } select { - case ch <- e: + case ch <- newUpdateEvent(correlationID, result, event.Err): case <-ctx.Done(): } }) @@ -110,10 +106,7 @@ func (s *serverIntentions) Notify(ctx context.Context, req *structs.ServiceSpeci sort.Sort(structs.IntentionPrecedenceSorter(intentions)) - return proxycfg.UpdateEvent{ - CorrelationID: correlationID, - Result: intentions, - }, true + return newUpdateEvent(correlationID, intentions, nil), true } for subjectIdx, subject := range subjects { diff --git a/agent/proxycfg/data_sources.go b/agent/proxycfg/data_sources.go index bda0226ffb..3649bed2d3 100644 --- a/agent/proxycfg/data_sources.go +++ b/agent/proxycfg/data_sources.go @@ -2,6 +2,7 @@ package proxycfg import ( "context" + "errors" cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/structs" @@ -15,6 +16,28 @@ type UpdateEvent struct { Err error } +// TerminalError wraps the given error to indicate that the data source is in +// an irrecoverably broken state (e.g. because the given ACL token has been +// deleted). +// +// Setting UpdateEvent.Err to a TerminalError causes all watches to be canceled +// which, in turn, terminates the xDS streams. +func TerminalError(err error) error { + return terminalError{err} +} + +// IsTerminalError returns whether the given error indicates that the data +// source is in an irrecoverably broken state so watches should be torn down +// and retried at a higher level. +func IsTerminalError(err error) bool { + return errors.As(err, &terminalError{}) +} + +type terminalError struct{ err error } + +func (e terminalError) Error() string { return e.err.Error() } +func (e terminalError) Unwrap() error { return e.err } + // DataSources contains the dependencies used to consume data used to configure // proxies. type DataSources struct { diff --git a/agent/proxycfg/manager.go b/agent/proxycfg/manager.go index 3de11b3f8a..efdfe4b724 100644 --- a/agent/proxycfg/manager.go +++ b/agent/proxycfg/manager.go @@ -127,7 +127,7 @@ func (m *Manager) Register(id ProxyID, ns *structs.NodeService, source ProxySour } // We are updating the proxy, close its old state - state.Close() + state.Close(false) } // TODO: move to a function that translates ManagerConfig->stateConfig @@ -148,14 +148,13 @@ func (m *Manager) Register(id ProxyID, ns *structs.NodeService, source ProxySour return err } - ch, err := state.Watch() - if err != nil { + if _, err = state.Watch(); err != nil { return err } m.proxies[id] = state // Start a goroutine that will wait for changes and broadcast them to watchers. - go m.notifyBroadcast(ch) + go m.notifyBroadcast(id, state) return nil } @@ -175,8 +174,8 @@ func (m *Manager) Deregister(id ProxyID, source ProxySource) { } // Closing state will let the goroutine we started in Register finish since - // watch chan is closed. - state.Close() + // watch chan is closed + state.Close(false) delete(m.proxies, id) // We intentionally leave potential watchers hanging here - there is no new @@ -186,11 +185,17 @@ func (m *Manager) Deregister(id ProxyID, source ProxySource) { // cleaned up naturally. } -func (m *Manager) notifyBroadcast(ch <-chan ConfigSnapshot) { - // Run until ch is closed - for snap := range ch { +func (m *Manager) notifyBroadcast(proxyID ProxyID, state *state) { + // Run until ch is closed (by a defer in state.run). + for snap := range state.snapCh { m.notify(&snap) } + + // If state.run exited because of an irrecoverable error, close all of the + // watchers so that the consumers reconnect/retry at a higher level. + if state.failed() { + m.closeAllWatchers(proxyID) + } } func (m *Manager) notify(snap *ConfigSnapshot) { @@ -281,6 +286,20 @@ func (m *Manager) Watch(id ProxyID) (<-chan *ConfigSnapshot, CancelFunc) { } } +func (m *Manager) closeAllWatchers(proxyID ProxyID) { + m.mu.Lock() + defer m.mu.Unlock() + + watchers, ok := m.watchers[proxyID] + if !ok { + return + } + + for watchID := range watchers { + m.closeWatchLocked(proxyID, watchID) + } +} + // closeWatchLocked cleans up state related to a single watcher. It assumes the // lock is held. func (m *Manager) closeWatchLocked(proxyID ProxyID, watchID uint64) { @@ -309,7 +328,7 @@ func (m *Manager) Close() error { // Then close all states for proxyID, state := range m.proxies { - state.Close() + state.Close(false) delete(m.proxies, proxyID) } return nil diff --git a/agent/proxycfg/state.go b/agent/proxycfg/state.go index 13b22c4fd2..34d3364356 100644 --- a/agent/proxycfg/state.go +++ b/agent/proxycfg/state.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "reflect" + "sync/atomic" "time" "github.com/hashicorp/go-hclog" @@ -70,11 +71,21 @@ type state struct { // in Watch. cancel func() + // failedFlag is (atomically) set to 1 (by Close) when run exits because a data + // source is in an irrecoverable state. It can be read with failed. + failedFlag int32 + ch chan UpdateEvent snapCh chan ConfigSnapshot reqCh chan chan *ConfigSnapshot } +// failed returns whether run exited because a data source is in an +// irrecoverable state. +func (s *state) failed() bool { + return atomic.LoadInt32(&s.failedFlag) == 1 +} + type DNSConfig struct { Domain string AltDomain string @@ -250,10 +261,13 @@ func (s *state) Watch() (<-chan ConfigSnapshot, error) { } // Close discards the state and stops any long-running watches. -func (s *state) Close() error { +func (s *state) Close(failed bool) error { if s.cancel != nil { s.cancel() } + if failed { + atomic.StoreInt32(&s.failedFlag, 1) + } return nil } @@ -300,7 +314,13 @@ func (s *state) run(ctx context.Context, snap *ConfigSnapshot) { case <-ctx.Done(): return case u := <-s.ch: - s.logger.Trace("A blocking query returned; handling snapshot update", "correlationID", u.CorrelationID) + s.logger.Trace("Data source returned; handling snapshot update", "correlationID", u.CorrelationID) + + if IsTerminalError(u.Err) { + s.logger.Error("Data source in an irrecoverable state; exiting", "error", u.Err, "correlationID", u.CorrelationID) + s.Close(true) + return + } if err := s.handler.handleUpdate(ctx, u, snap); err != nil { s.logger.Error("Failed to handle update from watch", diff --git a/agent/submatview/local_materializer.go b/agent/submatview/local_materializer.go index 6e32b36025..b3d4480bda 100644 --- a/agent/submatview/local_materializer.go +++ b/agent/submatview/local_materializer.go @@ -66,6 +66,10 @@ func (m *LocalMaterializer) Run(ctx context.Context) { if ctx.Err() != nil { return } + if m.isTerminalError(err) { + return + } + m.mat.handleError(req, err) if err := m.mat.retryWaiter.Wait(ctx); err != nil { @@ -74,6 +78,14 @@ func (m *LocalMaterializer) Run(ctx context.Context) { } } +// isTerminalError determines whether the given error cannot be recovered from +// and should cause the materializer to halt and be evicted from the view store. +// +// This roughly matches the logic in agent/proxycfg-glue.newUpdateEvent. +func (m *LocalMaterializer) isTerminalError(err error) bool { + return acl.IsErrNotFound(err) +} + // subscribeOnce opens a new subscription to a local backend and runs // for its lifetime or until the view is closed. func (m *LocalMaterializer) subscribeOnce(ctx context.Context, req *pbsubscribe.SubscribeRequest) error { diff --git a/agent/submatview/store.go b/agent/submatview/store.go index 242a0d70d7..dacf2d8bae 100644 --- a/agent/submatview/store.go +++ b/agent/submatview/store.go @@ -47,6 +47,9 @@ type entry struct { // requests is the count of active requests using this entry. This entry will // remain in the store as long as this count remains > 0. requests int + // evicting is used to mark an entry that will be evicted when the current in- + // flight requests finish. + evicting bool } // NewStore creates and returns a Store that is ready for use. The caller must @@ -89,6 +92,7 @@ func (s *Store) Run(ctx context.Context) { // Only stop the materializer if there are no active requests. if e.requests == 0 { + s.logger.Trace("evicting item from store", "key", he.Key()) e.stop() delete(s.byKey, he.Key()) } @@ -187,13 +191,13 @@ func (s *Store) NotifyCallback( "error", err, "request-type", req.Type(), "index", index) - continue } index = result.Index cb(ctx, cache.UpdateEvent{ CorrelationID: correlationID, Result: result.Value, + Err: err, Meta: cache.ResultMeta{Index: result.Index, Hit: result.Cached}, }) } @@ -211,6 +215,9 @@ func (s *Store) readEntry(req Request) (string, Materializer, error) { defer s.lock.Unlock() e, ok := s.byKey[key] if ok { + if e.evicting { + return "", nil, errors.New("item is marked for eviction") + } e.requests++ s.byKey[key] = e return key, e.materializer, nil @@ -222,7 +229,18 @@ func (s *Store) readEntry(req Request) (string, Materializer, error) { } ctx, cancel := context.WithCancel(context.Background()) - go mat.Run(ctx) + go func() { + mat.Run(ctx) + + // Materializers run until they either reach their TTL and are evicted (which + // cancels the given context) or encounter an irrecoverable error. + // + // If the context hasn't been canceled, we know it's the error case so we + // trigger an immediate eviction. + if ctx.Err() == nil { + s.evictNow(key) + } + }() e = entry{ materializer: mat, @@ -233,6 +251,28 @@ func (s *Store) readEntry(req Request) (string, Materializer, error) { return key, e.materializer, nil } +// evictNow causes the item with the given key to be evicted immediately. +// +// If there are requests in-flight, the item is marked for eviction such that +// once the requests have been served releaseEntry will move it to the top of +// the expiry heap. If there are no requests in-flight, evictNow will move the +// item to the top of the expiry heap itself. +// +// In either case, the entry's evicting flag prevents it from being served by +// readEntry (and thereby gaining new in-flight requests). +func (s *Store) evictNow(key string) { + s.lock.Lock() + defer s.lock.Unlock() + + e := s.byKey[key] + e.evicting = true + s.byKey[key] = e + + if e.requests == 0 { + s.expireNowLocked(key) + } +} + // releaseEntry decrements the request count and starts an expiry timer if the // count has reached 0. Must be called once for every call to readEntry. func (s *Store) releaseEntry(key string) { @@ -246,6 +286,11 @@ func (s *Store) releaseEntry(key string) { return } + if e.evicting { + s.expireNowLocked(key) + return + } + if e.expiry.Index() == ttlcache.NotIndexed { e.expiry = s.expiryHeap.Add(key, s.idleTTL) s.byKey[key] = e @@ -255,6 +300,17 @@ func (s *Store) releaseEntry(key string) { s.expiryHeap.Update(e.expiry.Index(), s.idleTTL) } +// expireNowLocked moves the item with the given key to the top of the expiry +// heap, causing it to be picked up by the expiry loop and evicted immediately. +func (s *Store) expireNowLocked(key string) { + e := s.byKey[key] + if idx := e.expiry.Index(); idx != ttlcache.NotIndexed { + s.expiryHeap.Remove(idx) + } + e.expiry = s.expiryHeap.Add(key, time.Duration(0)) + s.byKey[key] = e +} + // makeEntryKey matches agent/cache.makeEntryKey, but may change in the future. func makeEntryKey(typ string, r cache.RequestInfo) string { return fmt.Sprintf("%s/%s/%s/%s", typ, r.Datacenter, r.Token, r.Key) diff --git a/agent/submatview/store_test.go b/agent/submatview/store_test.go index 1d5789c054..aab0995998 100644 --- a/agent/submatview/store_test.go +++ b/agent/submatview/store_test.go @@ -509,3 +509,75 @@ func TestStore_Run_ExpiresEntries(t *testing.T) { require.Len(t, store.byKey, 0) require.Equal(t, ttlcache.NotIndexed, e.expiry.Index()) } + +func TestStore_Run_FailingMaterializer(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + store := NewStore(hclog.NewNullLogger()) + store.idleTTL = 24 * time.Hour + go store.Run(ctx) + + t.Run("with an in-flight request", func(t *testing.T) { + req := &failingMaterializerRequest{ + doneCh: make(chan struct{}), + } + + ch := make(chan cache.UpdateEvent) + reqCtx, reqCancel := context.WithCancel(context.Background()) + t.Cleanup(reqCancel) + require.NoError(t, store.Notify(reqCtx, req, "", ch)) + + assertRequestCount(t, store, req, 1) + + // Cause the materializer to "fail" (exit before its context is canceled). + close(req.doneCh) + + // End the in-flight request. + reqCancel() + + // Check that the item was evicted. + retry.Run(t, func(r *retry.R) { + store.lock.Lock() + defer store.lock.Unlock() + + require.Len(r, store.byKey, 0) + }) + }) + + t.Run("with no in-flight requests", func(t *testing.T) { + req := &failingMaterializerRequest{ + doneCh: make(chan struct{}), + } + + // Cause the materializer to "fail" (exit before its context is canceled). + close(req.doneCh) + + // Check that the item was evicted. + retry.Run(t, func(r *retry.R) { + store.lock.Lock() + defer store.lock.Unlock() + + require.Len(r, store.byKey, 0) + }) + }) +} + +type failingMaterializerRequest struct { + doneCh chan struct{} +} + +func (failingMaterializerRequest) CacheInfo() cache.RequestInfo { return cache.RequestInfo{} } +func (failingMaterializerRequest) Type() string { return "test.FailingMaterializerRequest" } + +func (r *failingMaterializerRequest) NewMaterializer() (Materializer, error) { + return &failingMaterializer{doneCh: r.doneCh}, nil +} + +type failingMaterializer struct { + doneCh <-chan struct{} +} + +func (failingMaterializer) Query(context.Context, uint64) (Result, error) { return Result{}, nil } + +func (m *failingMaterializer) Run(context.Context) { <-m.doneCh } diff --git a/agent/xds/delta.go b/agent/xds/delta.go index 701c04f2ed..71c1edcb0f 100644 --- a/agent/xds/delta.go +++ b/agent/xds/delta.go @@ -81,6 +81,11 @@ const ( ) func (s *Server) processDelta(stream ADSDeltaStream, reqCh <-chan *envoy_discovery_v3.DeltaDiscoveryRequest) error { + // Handle invalid ACL tokens up-front. + if _, err := s.authenticate(stream.Context()); err != nil { + return err + } + // Loop state var ( cfgSnap *proxycfg.ConfigSnapshot @@ -200,7 +205,18 @@ func (s *Server) processDelta(stream ADSDeltaStream, reqCh <-chan *envoy_discove } } - case cfgSnap = <-stateCh: + case cs, ok := <-stateCh: + if !ok { + // stateCh is closed either when *we* cancel the watch (on-exit via defer) + // or by the proxycfg.Manager when an irrecoverable error is encountered + // such as the ACL token getting deleted. + // + // We know for sure that this is the latter case, because in the former we + // would've already exited this loop. + return status.Error(codes.Aborted, "xDS stream terminated due to an irrecoverable error, please try again") + } + cfgSnap = cs + newRes, err := generator.allResourcesFromSnapshot(cfgSnap) if err != nil { return status.Errorf(codes.Unavailable, "failed to generate all xDS resources from the snapshot: %v", err) diff --git a/agent/xds/server.go b/agent/xds/server.go index cc27f3fde7..3ee42e77b0 100644 --- a/agent/xds/server.go +++ b/agent/xds/server.go @@ -186,6 +186,18 @@ func (s *Server) Register(srv *grpc.Server) { envoy_discovery_v3.RegisterAggregatedDiscoveryServiceServer(srv, s) } +func (s *Server) authenticate(ctx context.Context) (acl.Authorizer, error) { + authz, err := s.ResolveToken(external.TokenFromContext(ctx)) + if acl.IsErrNotFound(err) { + return nil, status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err) + } else if acl.IsErrPermissionDenied(err) { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } else if err != nil { + return nil, status.Errorf(codes.Internal, "error resolving acl token: %v", err) + } + return authz, nil +} + // authorize the xDS request using the token stored in ctx. This authorization is // a bit different from most interfaces. Instead of explicitly authorizing or // filtering each piece of data in the response, the request is authorized @@ -201,13 +213,9 @@ func (s *Server) authorize(ctx context.Context, cfgSnap *proxycfg.ConfigSnapshot return status.Errorf(codes.Unauthenticated, "unauthenticated: no config snapshot") } - authz, err := s.ResolveToken(external.TokenFromContext(ctx)) - if acl.IsErrNotFound(err) { - return status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err) - } else if acl.IsErrPermissionDenied(err) { - return status.Error(codes.PermissionDenied, err.Error()) - } else if err != nil { - return status.Errorf(codes.Internal, "error resolving acl token: %v", err) + authz, err := s.authenticate(ctx) + if err != nil { + return err } var authzContext acl.AuthorizerContext From 8d6b73aed0da7b2f181e5b97be88a44d4248bc60 Mon Sep 17 00:00:00 2001 From: Rosemary Wang <915624+joatmon08@users.noreply.github.com> Date: Tue, 23 Aug 2022 17:52:03 -0400 Subject: [PATCH 311/339] Clarify transparent proxy documentation (#14301) * Clarify transparent proxy documentation Some confusion over known limitations for transparent proxy, specifically over federation versus cluster peering. Updated `KubeDNS` to Kubernetes DNS for consistency with Kubernetes documentation. Co-authored-by: David Yu Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> --- .../docs/connect/cluster-peering/k8s.mdx | 9 ++-- .../docs/connect/transparent-proxy.mdx | 44 +++++++++++++------ 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/website/content/docs/connect/cluster-peering/k8s.mdx b/website/content/docs/connect/cluster-peering/k8s.mdx index 7471efed86..35f17959cb 100644 --- a/website/content/docs/connect/cluster-peering/k8s.mdx +++ b/website/content/docs/connect/cluster-peering/k8s.mdx @@ -132,7 +132,7 @@ To peer Kubernetes clusters running Consul, you need to create a peering token a ## Export services between clusters -1. For the service in "cluster-02" that you want to export, add the following [annotations](/docs/k8s/annotations-and-labels#consul-hashicorp-com-connect-service-upstreams) to your service's pods. +1. For the service in "cluster-02" that you want to export, add the following [annotation](/docs/k8s/annotations-and-labels) to your service's pods. @@ -140,7 +140,6 @@ To peer Kubernetes clusters running Consul, you need to create a peering token a ##… annotations: "consul.hashicorp.com/connect-inject": "true" - "consul.hashicorp.com/transparent-proxy": "false" ##… ``` @@ -207,8 +206,6 @@ To peer Kubernetes clusters running Consul, you need to create a peering token a ##… annotations: "consul.hashicorp.com/connect-inject": "true" - "consul.hashicorp.com/transparent-proxy": "false" - "consul.hashicorp.com/connect-service-upstreams": "backend-service.svc.cluster-02.peer:1234" ##… ``` @@ -220,10 +217,10 @@ To peer Kubernetes clusters running Consul, you need to create a peering token a $ kubectl apply --filename frontend-service.yml ``` -1. Run the following command and check the output to confirm that you peered your clusters successfully. +1. Run the following command in `frontend-service` and check the output to confirm that you peered your clusters successfully. ```shell-session - $ curl localhost:1234 + $ kubectl exec -it $(kubectl get pod -l app=frontend -o name) -- curl localhost:1234 { "name": "backend-service", ##… diff --git a/website/content/docs/connect/transparent-proxy.mdx b/website/content/docs/connect/transparent-proxy.mdx index 6e3353bbad..57ad48ba7a 100644 --- a/website/content/docs/connect/transparent-proxy.mdx +++ b/website/content/docs/connect/transparent-proxy.mdx @@ -31,7 +31,7 @@ With transparent proxy: 1. Local upstreams are inferred from service intentions and peered upstreams are inferred from imported services, so no explicit configuration is needed. -1. Outbound connections pointing to a KubeDNS name "just work" — network rules +1. Outbound connections pointing to a Kubernetes DNS record "just work" — network rules redirect them through the proxy. 1. Inbound traffic is forced to go through the proxy to prevent unauthorized direct access to the application. @@ -160,27 +160,43 @@ configure exceptions on a per-Pod basis. The following Pod annotations allow you - [`consul.hashicorp.com/transparent-proxy-exclude-uids`](/docs/k8s/annotations-and-labels#consul-hashicorp-com-transparent-proxy-exclude-uids) +### Dialing Services Across Kubernetes Clusters + +- You cannot use transparent proxy in a deployment configuration with [federation between Kubernetes clusters](/docs/k8s/installation/multi-cluster/kubernetes). + Instead, services in one Kubernetes cluster must explicitly dial a service to a Consul datacenter in another Kubernetes cluster using the + [consul.hashicorp.com/connect-service-upstreams](/docs/k8s/annotations-and-labels#consul-hashicorp-com-connect-service-upstreams) + annotation. For example, an annotation of + `"consul.hashicorp.com/connect-service-upstreams": "my-service:1234:dc2"` reaches an upstream service called `my-service` + in the datacenter `dc2` on port `1234`. + +- You cannot use transparent proxy in a deployment configuration with a + [single Consul datacenter spanning multiple Kubernetes clusters](/docs/k8s/installation/deployment-configurations/single-dc-multi-k8s). Instead, + services in one Kubernetes cluster must explicitly dial a service in another Kubernetes cluster using the + [consul.hashicorp.com/connect-service-upstreams](/docs/k8s/annotations-and-labels#consul-hashicorp-com-connect-service-upstreams) + annotation. For example, an annotation of + `"consul.hashicorp.com/connect-service-upstreams": "my-service:1234"`, + reaches an upstream service called `my-service` in another Kubernetes cluster and on port `1234`. + Although transparent proxy is enabled, Kubernetes DNS is not utilized when communicating between services that exist on separate Kubernetes clusters. + +- In a deployment configuration with [cluster peering](/docs/connect/cluster-peering), + transparent proxy is fully supported and thus dialing services explicitly is not required. + + ## Known Limitations -* Traffic can only be transparently proxied when the address dialed corresponds to the address of a service in the -transparent proxy's datacenter. Services can also dial explicit upstreams in other datacenters without transparent proxy, for example, by adding an -[annotation](/docs/k8s/annotations-and-labels#consul-hashicorp-com-connect-service-upstreams) such as -`"consul.hashicorp.com/connect-service-upstreams": "my-service:1234:dc2"` to reach an upstream service called `my-service` -in the datacenter `dc2`. -* In the deployment configuration where a [single Consul datacenter spans multiple Kubernetes clusters](/docs/k8s/installation/deployment-configurations/single-dc-multi-k8s), services in one Kubernetes cluster must explicitly dial a service in another Kubernetes cluster using the [consul.hashicorp.com/connect-service-upstreams](/docs/k8s/annotations-and-labels#consul-hashicorp-com-connect-service-upstreams) annotation. An example would be -`"consul.hashicorp.com/connect-service-upstreams": "my-service:1234"`, where `my-service` is the service that exists in another Kubernetes cluster and is exposed on port `1234`. Although Transparent Proxy is enabled, KubeDNS is not utilized when communicating between services existing on separate Kubernetes clusters. +- Deployment configurations with federation across or a single datacenter spanning multiple clusters must explicitly dial a + service in another datacenter or cluster using annotations. -* When dialing headless services, the request will be proxied using a plain TCP - proxy. The upstream's protocol is not considered. +- When dialing headless services, the request is proxied using a plain TCP proxy. The upstream's protocol is not considered. ## Using Transparent Proxy In Kubernetes, services can reach other services via their -[KubeDNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) address or via Pod IPs, and that +[Kubernetes DNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) address or through Pod IPs, and that traffic will be transparently sent through the proxy. Connect services in Kubernetes are required to have a Kubernetes service selecting the Pods. -~> Note: In order to use KubeDNS, the Kubernetes service name will need to match the Consul service name. This will be the +~> **Note**: In order to use Kubernetes DNS, the Kubernetes service name needs to match the Consul service name. This is the case by default, unless the service Pods have the annotation `consul.hashicorp.com/connect-service` overriding the Consul service name. @@ -192,7 +208,7 @@ inbound and outbound listener on the sidecar proxy. The proxy will be configured appropriate upstream services based on [Service Intentions](/docs/connect/config-entries/service-intentions). This means Connect services no longer need to use the `consul.hashicorp.com/connect-service-upstreams` annotation to configure upstreams explicitly. Once the -Service Intentions are set, they can simply address the upstream services using KubeDNS. +Service Intentions are set, they can simply address the upstream services using Kubernetes DNS. As of Consul-k8s >= `0.26.0` and Consul-helm >= `0.32.0`, a Kubernetes service that selects application pods is required for Connect applications, i.e: @@ -213,7 +229,7 @@ spec: In the example above, if another service wants to reach `sample-app` via transparent proxying, it can dial `sample-app.default.svc.cluster.local`, using -[KubeDNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/). +[Kubernetes DNS](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/). If ACLs with default "deny" policy are enabled, it also needs a [ServiceIntention](/docs/connect/config-entries/service-intentions) allowing it to talk to `sample-app`. From bb35a8303ddcd2758de575326502f6a232a4cc97 Mon Sep 17 00:00:00 2001 From: twunderlich-grapl <88346193+twunderlich-grapl@users.noreply.github.com> Date: Tue, 23 Aug 2022 20:06:00 -0400 Subject: [PATCH 312/339] Clarify docs around using either Consul or Vault managed PKI paths (#13295) * Clarify docs around using either Consul or Vault managed PKI paths The current docs can be misread to indicate that you need both the Consul and Vault managed PKI Paths policies. The [Learning Tutorial](https://learn.hashicorp.com/tutorials/consul/vault-pki-consul-connect-ca?in=consul/vault-secure#create-vault-policies) is clearer. This tries to make the original docs as clear as the learning tutorial * Clarify that PKI secret engines are used to store certs Co-authored-by: Blake Covarrubias --- website/content/docs/connect/ca/vault.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/content/docs/connect/ca/vault.mdx b/website/content/docs/connect/ca/vault.mdx index e0a9daa6ea..e563a6d83d 100644 --- a/website/content/docs/connect/ca/vault.mdx +++ b/website/content/docs/connect/ca/vault.mdx @@ -201,6 +201,8 @@ If the paths already exist, Consul will use them as configured. ## Vault ACL Policies +Vault PKI can be managed by either Consul or by Vault. If you want to manually create and tune the PKI secret engines used to store the root and intermediate certificates, use Vault Managed PKI Paths. If you want to have the PKI automatically managed for you, use Consul Managed PKI Paths. + ### Vault Managed PKI Paths The following Vault policy allows Consul to use pre-existing PKI paths in Vault. From 3b993f2da77534c8b6463227bbfe5a0f382deb27 Mon Sep 17 00:00:00 2001 From: Dan Upton Date: Wed, 24 Aug 2022 12:03:15 +0100 Subject: [PATCH 313/339] dataplane: update envoy bootstrap params for consul-dataplane (#14017) Contains 2 changes to the GetEnvoyBootstrapParams response to support consul-dataplane. Exposing node_name and node_id: consul-dataplane will support providing either the node_id or node_name in its configuration. Unfortunately, supporting both in the xDS meta adds a fair amount of complexity (partly because most tables are currently indexed on node_name) so for now we're going to return them both from the bootstrap params endpoint, allowing consul-dataplane to exchange a node_id for a node_name (which it will supply in the xDS meta). Properly setting service for gateways: To avoid the need to special case gateways in consul-dataplane, service will now either be the destination service name for connect proxies, or the gateway service name. This means it can be used as-is in Envoy configuration (i.e. as a cluster name or in metric tags). --- agent/consul/state/catalog.go | 3 + agent/consul/state/catalog_test.go | 7 +- .../dataplane/get_envoy_bootstrap_params.go | 10 +- ....go => get_envoy_bootstrap_params_test.go} | 10 +- proto-public/pbdataplane/dataplane.pb.go | 151 ++++++++++-------- proto-public/pbdataplane/dataplane.proto | 7 +- 6 files changed, 118 insertions(+), 70 deletions(-) rename agent/grpc-external/services/dataplane/{get_envoy_boostrap_params_test.go => get_envoy_bootstrap_params_test.go} (96%) diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 258519d5ba..f9483a313f 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -1717,6 +1717,9 @@ func (s *Store) ServiceNode(nodeID, nodeName, serviceID string, entMeta *acl.Ent if err != nil { return 0, nil, fmt.Errorf("failed querying service for node %q: %w", node.Node, err) } + if service != nil { + service.ID = node.ID + } return idx, service, nil } diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index 10e7af6dba..1e096d136f 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -270,17 +270,20 @@ func TestStateStore_EnsureRegistration(t *testing.T) { require.Equal(t, uint64(2), idx) require.Equal(t, svcmap["redis1"], r) + exp := svcmap["redis1"].ToServiceNode("node1") + exp.ID = nodeID + // lookup service by node name idx, sn, err := s.ServiceNode("", "node1", "redis1", nil, peerName) require.NoError(t, err) require.Equal(t, uint64(2), idx) - require.Equal(t, svcmap["redis1"].ToServiceNode("node1"), sn) + require.Equal(t, exp, sn) // lookup service by node ID idx, sn, err = s.ServiceNode(string(nodeID), "", "redis1", nil, peerName) require.NoError(t, err) require.Equal(t, uint64(2), idx) - require.Equal(t, svcmap["redis1"].ToServiceNode("node1"), sn) + require.Equal(t, exp, sn) // lookup service by invalid node _, _, err = s.ServiceNode("", "invalid-node", "redis1", nil, peerName) diff --git a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go index bed302d12b..b320559e98 100644 --- a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go +++ b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go @@ -52,13 +52,21 @@ func (s *Server) GetEnvoyBootstrapParams(ctx context.Context, req *pbdataplane.G } // Build out the response + var serviceName string + if svc.ServiceKind == structs.ServiceKindConnectProxy { + serviceName = svc.ServiceProxy.DestinationServiceName + } else { + serviceName = svc.ServiceName + } resp := &pbdataplane.GetEnvoyBootstrapParamsResponse{ - Service: svc.ServiceProxy.DestinationServiceName, + Service: serviceName, Partition: svc.EnterpriseMeta.PartitionOrDefault(), Namespace: svc.EnterpriseMeta.NamespaceOrDefault(), Datacenter: s.Datacenter, ServiceKind: convertToResponseServiceKind(svc.ServiceKind), + NodeName: svc.Node, + NodeId: string(svc.ID), } bootstrapConfig, err := structpb.NewStruct(svc.ServiceProxy.Config) diff --git a/agent/grpc-external/services/dataplane/get_envoy_boostrap_params_test.go b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go similarity index 96% rename from agent/grpc-external/services/dataplane/get_envoy_boostrap_params_test.go rename to agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go index c3b4fd1468..aa42b0bf13 100644 --- a/agent/grpc-external/services/dataplane/get_envoy_boostrap_params_test.go +++ b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go @@ -97,14 +97,20 @@ func TestGetEnvoyBootstrapParams_Success(t *testing.T) { resp, err := client.GetEnvoyBootstrapParams(ctx, req) require.NoError(t, err) - require.Equal(t, tc.registerReq.Service.Proxy.DestinationServiceName, resp.Service) + if tc.registerReq.Service.IsGateway() { + require.Equal(t, tc.registerReq.Service.Service, resp.Service) + } else { + require.Equal(t, tc.registerReq.Service.Proxy.DestinationServiceName, resp.Service) + } + require.Equal(t, serverDC, resp.Datacenter) require.Equal(t, tc.registerReq.EnterpriseMeta.PartitionOrDefault(), resp.Partition) require.Equal(t, tc.registerReq.EnterpriseMeta.NamespaceOrDefault(), resp.Namespace) require.Contains(t, resp.Config.Fields, proxyConfigKey) require.Equal(t, structpb.NewStringValue(proxyConfigValue), resp.Config.Fields[proxyConfigKey]) require.Equal(t, convertToResponseServiceKind(tc.registerReq.Service.Kind), resp.ServiceKind) - + require.Equal(t, tc.registerReq.Node, resp.NodeName) + require.Equal(t, string(tc.registerReq.ID), resp.NodeId) } testCases := []testCase{ diff --git a/proto-public/pbdataplane/dataplane.pb.go b/proto-public/pbdataplane/dataplane.pb.go index 1da1eea15a..8e8a1000f2 100644 --- a/proto-public/pbdataplane/dataplane.pb.go +++ b/proto-public/pbdataplane/dataplane.pb.go @@ -401,12 +401,17 @@ type GetEnvoyBootstrapParamsResponse struct { unknownFields protoimpl.UnknownFields ServiceKind ServiceKind `protobuf:"varint,1,opt,name=service_kind,json=serviceKind,proto3,enum=hashicorp.consul.dataplane.ServiceKind" json:"service_kind,omitempty"` - // The destination service name + // service is be used to identify the service (as the local cluster name and + // in metric tags). If the service is a connect proxy it will be the name of + // the proxy's destination service, for gateways it will be the gateway + // service's name. Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` Partition string `protobuf:"bytes,4,opt,name=partition,proto3" json:"partition,omitempty"` Datacenter string `protobuf:"bytes,5,opt,name=datacenter,proto3" json:"datacenter,omitempty"` Config *structpb.Struct `protobuf:"bytes,6,opt,name=config,proto3" json:"config,omitempty"` + NodeId string `protobuf:"bytes,7,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + NodeName string `protobuf:"bytes,8,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"` } func (x *GetEnvoyBootstrapParamsResponse) Reset() { @@ -483,6 +488,20 @@ func (x *GetEnvoyBootstrapParamsResponse) GetConfig() *structpb.Struct { return nil } +func (x *GetEnvoyBootstrapParamsResponse) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *GetEnvoyBootstrapParamsResponse) GetNodeName() string { + if x != nil { + return x.NodeName + } + return "" +} + var File_proto_public_pbdataplane_dataplane_proto protoreflect.FileDescriptor var file_proto_public_pbdataplane_dataplane_proto_rawDesc = []byte{ @@ -525,7 +544,7 @@ var file_proto_public_pbdataplane_dataplane_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x22, 0x94, + 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x22, 0xca, 0x02, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6b, 0x69, @@ -543,69 +562,73 @@ var file_proto_public_pbdataplane_dataplane_proto_rawDesc = []byte{ 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2a, 0xc7, 0x01, 0x0a, 0x11, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, - 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x44, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x1b, + 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x2a, 0xc7, 0x01, 0x0a, 0x11, + 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, + 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, + 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x57, 0x41, 0x54, 0x43, + 0x48, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x10, 0x01, 0x12, 0x32, 0x0a, 0x2e, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, - 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x24, 0x0a, 0x20, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, - 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x57, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x45, 0x52, 0x56, - 0x45, 0x52, 0x53, 0x10, 0x01, 0x12, 0x32, 0x0a, 0x2e, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, - 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x45, 0x44, 0x47, 0x45, - 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x4d, 0x41, 0x4e, - 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x34, 0x0a, 0x30, 0x44, 0x41, 0x54, - 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, - 0x45, 0x4e, 0x56, 0x4f, 0x59, 0x5f, 0x42, 0x4f, 0x4f, 0x54, 0x53, 0x54, 0x52, 0x41, 0x50, 0x5f, - 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, - 0xcc, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, - 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, - 0x14, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x59, - 0x50, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x45, 0x52, 0x56, 0x49, - 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, - 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x45, 0x52, 0x56, 0x49, - 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x45, 0x53, 0x48, 0x5f, 0x47, 0x41, 0x54, - 0x45, 0x57, 0x41, 0x59, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x49, - 0x4e, 0x47, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x10, 0x04, 0x12, 0x20, 0x0a, 0x1c, - 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x49, 0x4e, 0x47, - 0x52, 0x45, 0x53, 0x53, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x10, 0x05, 0x32, 0xd2, - 0x02, 0x0a, 0x10, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, - 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, - 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, - 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, - 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, - 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x94, 0x01, 0x0a, - 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, - 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, - 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, - 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, - 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x42, 0xf0, 0x01, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, - 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x42, 0x0e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x2f, 0x70, 0x62, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0xa2, 0x02, - 0x03, 0x48, 0x43, 0x44, 0xaa, 0x02, 0x1a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0xca, 0x02, 0x1a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0xe2, 0x02, - 0x26, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1c, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x44, 0x61, 0x74, - 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x53, 0x5f, 0x45, 0x44, 0x47, 0x45, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x12, + 0x34, 0x0a, 0x30, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, + 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x45, 0x4e, 0x56, 0x4f, 0x59, 0x5f, 0x42, 0x4f, 0x4f, 0x54, + 0x53, 0x54, 0x52, 0x41, 0x50, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, 0xcc, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, + 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, + 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x59, 0x50, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x1e, 0x0a, + 0x1a, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, + 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x02, 0x12, 0x1d, 0x0a, + 0x19, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x45, + 0x53, 0x48, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, + 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x45, 0x52, + 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, + 0x10, 0x04, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, + 0x4e, 0x44, 0x5f, 0x49, 0x4e, 0x47, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, + 0x41, 0x59, 0x10, 0x05, 0x32, 0xd2, 0x02, 0x0a, 0x10, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, + 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x47, 0x65, + 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, + 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x94, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, + 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x3a, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, + 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, + 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, + 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xf0, 0x01, 0x0a, 0x1e, 0x63, 0x6f, + 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x42, 0x0e, 0x44, 0x61, + 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x34, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x64, 0x61, 0x74, 0x61, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0xa2, 0x02, 0x03, 0x48, 0x43, 0x44, 0xaa, 0x02, 0x1a, 0x48, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x44, 0x61, + 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0xca, 0x02, 0x1a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0xe2, 0x02, 0x26, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1c, + 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x3a, 0x3a, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto-public/pbdataplane/dataplane.proto b/proto-public/pbdataplane/dataplane.proto index 0502dcd707..cc95f3a517 100644 --- a/proto-public/pbdataplane/dataplane.proto +++ b/proto-public/pbdataplane/dataplane.proto @@ -68,12 +68,17 @@ enum ServiceKind { message GetEnvoyBootstrapParamsResponse { ServiceKind service_kind = 1; - // The destination service name + // service is be used to identify the service (as the local cluster name and + // in metric tags). If the service is a connect proxy it will be the name of + // the proxy's destination service, for gateways it will be the gateway + // service's name. string service = 2; string namespace = 3; string partition = 4; string datacenter = 5; google.protobuf.Struct config = 6; + string node_id = 7; + string node_name = 8; } service DataplaneService { From cdc6fd89d3c3e21aef032df77e57a47051f308e8 Mon Sep 17 00:00:00 2001 From: Tyler Wendlandt Date: Wed, 24 Aug 2022 06:44:01 -0600 Subject: [PATCH 314/339] ui: Replace file-mask with file-text icon usage on policy list (#14275) --- ui/.gitignore | 1 + ui/packages/consul-ui/app/components/composite-row/index.scss | 2 +- ui/packages/consul-ui/app/styles/base/icons/icons/index.scss | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/.gitignore b/ui/.gitignore index 08df27ddb9..6bd9a0135a 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -12,6 +12,7 @@ node_modules .pnp* .sass-cache .DS_Store +.tool-versions connect.lock coverage coverage_* diff --git a/ui/packages/consul-ui/app/components/composite-row/index.scss b/ui/packages/consul-ui/app/components/composite-row/index.scss index bd66491a7f..1dce70e4bb 100644 --- a/ui/packages/consul-ui/app/components/composite-row/index.scss +++ b/ui/packages/consul-ui/app/components/composite-row/index.scss @@ -95,7 +95,7 @@ } %composite-row-detail .policy::before { - @extend %with-file-fill-mask, %as-pseudo; + @extend %with-file-text-mask, %as-pseudo; margin-right: 3px; } %composite-row-detail .role::before { diff --git a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss index 9d1a5efe3f..20f57edc7a 100644 --- a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss +++ b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss @@ -330,7 +330,7 @@ // @import './file-minus/index.scss'; // @import './file-plus/index.scss'; // @import './file-source/index.scss'; -// @import './file-text/index.scss'; +@import './file-text/index.scss'; // @import './file-x/index.scss'; // @import './files/index.scss'; // @import './film/index.scss'; From ca228aad8dbf246e57c90916853a792297d457b7 Mon Sep 17 00:00:00 2001 From: DanStough Date: Fri, 19 Aug 2022 16:51:11 -0400 Subject: [PATCH 315/339] doc: tproxy destination fixes --- .../config-entries/terminating-gateway.mdx | 5 +-- .../docs/k8s/connect/terminating-gateways.mdx | 36 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/website/content/docs/connect/config-entries/terminating-gateway.mdx b/website/content/docs/connect/config-entries/terminating-gateway.mdx index 3692eff1ec..c406c5687d 100644 --- a/website/content/docs/connect/config-entries/terminating-gateway.mdx +++ b/website/content/docs/connect/config-entries/terminating-gateway.mdx @@ -153,8 +153,9 @@ spec: Link gateway named "us-west-gateway" with the billing service, and specify a CA file to be used for one-way TLS authentication. --> **Note**: The `CAFile` parameter must be specified _and_ point to a valid CA -bundle in order to properly initiate a TLS connection to the destination service. +-> **Note**: When not using destinations in transparent proxy mode, you must specify the `CAFile` parameter +and point to a valid CA bundle in order to properly initiate a TLS +connection to the destination service. For more information about configuring a gateway for destinations, refer to [Register an External Service as a Destination](/docs/k8s/connect/terminating-gateways#register-an-external-service-as-a-destination). diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 13da908b4f..e82bd773fb 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -89,13 +89,13 @@ Registering the external services with Consul is a multi-step process: ### Register external services with Consul There are two ways to register an external service with Consul: -1. If [`TransparentProxy`](/docs/k8s/helm#v-connectinject-transparentproxy) is enabled, you can declare external endpoints in the [`Destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of `service-defaults`. +1. If [`TransparentProxy`](/docs/connect/transparent-proxy) is enabled, the preferred method is to declare external endpoints in the [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of `ServiceDefaults`. 1. You can add the service as a node in the Consul catalog. -#### Register an external service as a Destination +#### Register an external service as a destination -`Destination` fields allow clients to dial the external service directly and are valid only in [`TransparentProxy`](/docs/k8s/helm#v-connectinject-transparentproxy) mode. -The following table describes traffic behaviors when using `Destination`s to route traffic through a terminating gateway: +The [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of the `ServiceDefaults` Custom Resource Definition (CRD) allows clients to dial the external service directly. It is valid only in [`TransparentProxy`](/docs/connect/transparent-proxy)) mode. +The following table describes traffic behaviors when using `destination`s to route traffic through a terminating gateway: | External Services Layer | Client dials | Client uses TLS | Allowed | Notes | |---|---|---|---|---| @@ -109,11 +109,13 @@ The following table describes traffic behaviors when using `Destination`s to rou | L7 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | You can provide a `caFile` to secure traffic between unencrypted clients that connect to external services through the terminating gateway. -Refer to [Create the configuration entry for the terminating gateway](/docs/k8s/connect/terminating-gateways#create-the-configuration-entry-for-the-terminating-gateway) for details. +Refer to [Create the configuration entry for the terminating gateway](#create-the-configuration-entry-for-the-terminating-gateway) for details. -Create a `service-defaults` custom resource for the external service: +Also note that regardless of the `protocol` specified in the `ServiceDefaults`, [L7 intentions](/docs/connect/config-entries/service-intentions#permissions) are not currently supported with `ServiceDefaults` destinations. - +Create a `ServiceDefaults` custom resource for the external service: + + ```yaml apiVersion: consul.hashicorp.com/v1alpha1 @@ -133,10 +135,10 @@ Create a `service-defaults` custom resource for the external service: Apply the `ServiceDefaults` resource with `kubectl apply`: ```shell-session -$ kubectl apply --filename service-defaults.yaml +$ kubectl apply --filename serviceDefaults.yaml ``` -All other terminating gateway operations can use the name of the `service-defaults` in place of a typical Consul service name. +All other terminating gateway operations can use the name of the `ServiceDefaults` in place of a typical Consul service name. #### Register an external service as a Catalog Node @@ -261,11 +263,13 @@ spec: --> **NOTE**: If TLS is enabled for external services registered through the Consul catalog, you must include the `caFile` parameter that points to the system trust store of the terminating gateway container. +If TLS is enabled for external services registered through the Consul catalog and you are not using [transparent proxy `destination`](#register-an-external-service-as-a-destination), you must include the [`caFile`](/docs/connect/config-entries/terminating-gateway#cafile) parameter that points to the system trust store of the terminating gateway container. By default, the trust store is located in the `/etc/ssl/certs/ca-certificates.crt` directory. -Configure the `caFile` parameter to point to the `/etc/ssl/cert.pem` directory if TLS is enabled and you are using one of the following components: - * Consul Helm chart 0.43 or older - * Or an Envoy image with an alpine base image +Configure the [`caFile`](https://www.consul.io/docs/connect/config-entries/terminating-gateway#cafile) parameter in the `TerminatingGateway` config entry to point to the `/etc/ssl/cert.pem` directory if TLS is enabled and you are using one of the following components: +- Consul Helm chart 0.43 or older +- An Envoy image with an alpine base image + +For `ServiceDefaults` destinations, refer to [Register an external service as a destination](#register-an-external-service-as-a-destination). Apply the `TerminatingGateway` resource with `kubectl apply`: @@ -273,7 +277,7 @@ Apply the `TerminatingGateway` resource with `kubectl apply`: $ kubectl apply --filename terminating-gateway.yaml ``` -If using ACLs and TLS, create a [`ServiceIntentions`](/docs/connect/config-entries/service-intentions) resource to allow access from services in the mesh to the external service +If using ACLs and TLS, create a [`ServiceIntentions`](/docs/connect/config-entries/service-intentions) resource to allow access from services in the mesh to the external service: @@ -292,6 +296,8 @@ spec: +-> **NOTE**: [L7 Intentions](/docs/connect/config-entries/service-intentions#permissions) are not currently supported for `ServiceDefaults` destinations. + Apply the `ServiceIntentions` resource with `kubectl apply`: ```shell-session @@ -372,7 +378,7 @@ $ kubectl exec deploy/static-client -- curl -vvvs --header "Host: example-https. - + ```shell-session $ kubectl exec deploy/static-client -- curl -vvvs https://example.com/ From 1f293e52440942b5a78e0381a80e362b3df6b763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Ruiz=20Garc=C3=ADa?= Date: Wed, 24 Aug 2022 18:31:38 +0200 Subject: [PATCH 316/339] Added new auto_encrypt.grpc_server_tls config option to control AutoTLS enabling of GRPC Server's TLS usage Fix for #14253 Co-authored-by: trujillo-adam <47586768+trujillo-adam@users.noreply.github.com> --- agent/config/builder.go | 8 +- agent/config/config.go | 1 + agent/config/runtime_test.go | 103 ++++++++++++++++-- .../TestRuntimeConfig_Sanitize.golden | 11 +- agent/config/testdata/full-config.hcl | 1 + agent/config/testdata/full-config.json | 3 +- agent/grpc-external/server.go | 5 +- tlsutil/config.go | 20 +++- tlsutil/config_test.go | 41 +++++-- .../docs/agent/config/config-files.mdx | 2 + 10 files changed, 162 insertions(+), 33 deletions(-) diff --git a/agent/config/builder.go b/agent/config/builder.go index 40389553d2..960d86ea43 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -2531,10 +2531,9 @@ func (b *builder) buildTLSConfig(rt RuntimeConfig, t TLS) (tlsutil.Config, error return c, errors.New("verify_server_hostname is only valid in the tls.internal_rpc stanza") } - // TLS is only enabled on the gRPC listener if there's an HTTPS port configured - // for historic and backwards-compatibility reasons. - if rt.HTTPSPort <= 0 && (t.GRPC != TLSProtocolConfig{} && t.GRPCModifiedByDeprecatedConfig == nil) { - b.warn("tls.grpc was provided but TLS will NOT be enabled on the gRPC listener without an HTTPS listener configured (e.g. via ports.https)") + // And UseAutoCert right now only applies to external gRPC interface. + if t.Defaults.UseAutoCert != nil || t.HTTPS.UseAutoCert != nil || t.InternalRPC.UseAutoCert != nil { + return c, errors.New("use_auto_cert is only valid in the tls.grpc stanza") } defaultTLSMinVersion := b.tlsVersion("tls.defaults.tls_min_version", t.Defaults.TLSMinVersion) @@ -2591,6 +2590,7 @@ func (b *builder) buildTLSConfig(rt RuntimeConfig, t TLS) (tlsutil.Config, error mapCommon("https", t.HTTPS, &c.HTTPS) mapCommon("grpc", t.GRPC, &c.GRPC) + c.GRPC.UseAutoCert = boolValWithDefault(t.GRPC.UseAutoCert, false) c.ServerName = rt.ServerName c.NodeName = rt.NodeName diff --git a/agent/config/config.go b/agent/config/config.go index 145c74db7c..2d21e75dae 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -867,6 +867,7 @@ type TLSProtocolConfig struct { VerifyIncoming *bool `mapstructure:"verify_incoming"` VerifyOutgoing *bool `mapstructure:"verify_outgoing"` VerifyServerHostname *bool `mapstructure:"verify_server_hostname"` + UseAutoCert *bool `mapstructure:"use_auto_cert"` } type TLS struct { diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index e0266811e3..f5e9bd3352 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -5516,7 +5516,70 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { }, }) run(t, testCase{ - desc: "tls.grpc without ports.https", + desc: "tls.grpc.use_auto_cert defaults to false", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + "tls": { + "grpc": {} + } + } + `}, + hcl: []string{` + tls { + grpc {} + } + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false + }, + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert defaults to false (II)", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + "tls": {} + } + `}, + hcl: []string{` + tls { + } + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false + }, + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert defaults to false (III)", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + } + `}, + hcl: []string{` + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false + }, + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert enabled when true", args: []string{ `-data-dir=` + dataDir, }, @@ -5524,7 +5587,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { { "tls": { "grpc": { - "cert_file": "cert-1234" + "use_auto_cert": true } } } @@ -5532,20 +5595,43 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { hcl: []string{` tls { grpc { - cert_file = "cert-1234" + use_auto_cert = true } } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir - rt.TLS.Domain = "consul." rt.TLS.NodeName = "thehostname" - - rt.TLS.GRPC.CertFile = "cert-1234" + rt.TLS.GRPC.UseAutoCert = true }, - expectedWarnings: []string{ - "tls.grpc was provided but TLS will NOT be enabled on the gRPC listener without an HTTPS listener configured (e.g. via ports.https)", + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert disabled when false", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + "tls": { + "grpc": { + "use_auto_cert": false + } + } + } + `}, + hcl: []string{` + tls { + grpc { + use_auto_cert = false + } + } + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false }, }) } @@ -6340,6 +6426,7 @@ func TestLoad_FullConfig(t *testing.T) { TLSMinVersion: types.TLSv1_0, CipherSuites: []types.TLSCipherSuite{types.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, types.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA}, VerifyOutgoing: false, + UseAutoCert: true, }, HTTPS: tlsutil.ProtocolConfig{ VerifyIncoming: true, diff --git a/agent/config/testdata/TestRuntimeConfig_Sanitize.golden b/agent/config/testdata/TestRuntimeConfig_Sanitize.golden index 09ecd4cfeb..8f91743dba 100644 --- a/agent/config/testdata/TestRuntimeConfig_Sanitize.golden +++ b/agent/config/testdata/TestRuntimeConfig_Sanitize.golden @@ -374,7 +374,8 @@ "TLSMinVersion": "", "VerifyIncoming": false, "VerifyOutgoing": false, - "VerifyServerHostname": false + "VerifyServerHostname": false, + "UseAutoCert": false }, "HTTPS": { "CAFile": "", @@ -385,7 +386,8 @@ "TLSMinVersion": "", "VerifyIncoming": false, "VerifyOutgoing": false, - "VerifyServerHostname": false + "VerifyServerHostname": false, + "UseAutoCert": false }, "InternalRPC": { "CAFile": "", @@ -396,7 +398,8 @@ "TLSMinVersion": "", "VerifyIncoming": false, "VerifyOutgoing": false, - "VerifyServerHostname": false + "VerifyServerHostname": false, + "UseAutoCert": false }, "NodeName": "", "ServerName": "" @@ -466,4 +469,4 @@ "VersionMetadata": "", "VersionPrerelease": "", "Watches": [] -} \ No newline at end of file +} diff --git a/agent/config/testdata/full-config.hcl b/agent/config/testdata/full-config.hcl index ed8203296c..305df9b89e 100644 --- a/agent/config/testdata/full-config.hcl +++ b/agent/config/testdata/full-config.hcl @@ -697,6 +697,7 @@ tls { tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" tls_min_version = "TLSv1_0" verify_incoming = true + use_auto_cert = true } } tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" diff --git a/agent/config/testdata/full-config.json b/agent/config/testdata/full-config.json index 8294a27b7c..bc72c2955e 100644 --- a/agent/config/testdata/full-config.json +++ b/agent/config/testdata/full-config.json @@ -692,7 +692,8 @@ "key_file": "1y4prKjl", "tls_cipher_suites": "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "tls_min_version": "TLSv1_0", - "verify_incoming": true + "verify_incoming": true, + "use_auto_cert": true } }, "tls_cipher_suites": "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", diff --git a/agent/grpc-external/server.go b/agent/grpc-external/server.go index 751cca91c8..4ae8c6d652 100644 --- a/agent/grpc-external/server.go +++ b/agent/grpc-external/server.go @@ -1,12 +1,13 @@ package external import ( + "time" + middleware "github.com/grpc-ecosystem/go-grpc-middleware" recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" - "time" agentmiddleware "github.com/hashicorp/consul/agent/grpc-middleware" "github.com/hashicorp/consul/tlsutil" @@ -34,7 +35,7 @@ func NewServer(logger agentmiddleware.Logger, tls *tlsutil.Configurator) *grpc.S MinTime: 15 * time.Second, }), } - if tls != nil && tls.GRPCTLSConfigured() { + if tls != nil && tls.GRPCServerUseTLS() { creds := credentials.NewTLS(tls.IncomingGRPCConfig()) opts = append(opts, grpc.Creds(creds)) } diff --git a/tlsutil/config.go b/tlsutil/config.go index 7c9e6d2ad6..2e1614165e 100644 --- a/tlsutil/config.go +++ b/tlsutil/config.go @@ -102,6 +102,10 @@ type ProtocolConfig struct { // // Note: this setting only applies to the Internal RPC configuration. VerifyServerHostname bool + + // UseAutoCert is used to enable usage of auto_encrypt/auto_config generated + // certificate & key material on external gRPC listener. + UseAutoCert bool } // Config configures the Configurator. @@ -167,6 +171,10 @@ type protocolConfig struct { // combinedCAPool is a pool containing both manualCAPEMs and the certificates // received from auto-config/auto-encrypt. combinedCAPool *x509.CertPool + + // useAutoCert indicates wether we should use auto-encrypt/config data + // for TLS server/listener. NOTE: Only applies to external GRPC Server. + useAutoCert bool } // Configurator provides tls.Config and net.Dial wrappers to enable TLS for @@ -323,6 +331,7 @@ func (c *Configurator) loadProtocolConfig(base Config, pc ProtocolConfig) (*prot manualCAPEMs: pems, manualCAPool: manualPool, combinedCAPool: combinedPool, + useAutoCert: pc.UseAutoCert, }, nil } @@ -620,16 +629,15 @@ func (c *Configurator) Cert() *tls.Certificate { return cert } -// GRPCTLSConfigured returns whether there's a TLS certificate configured for -// gRPC (either manually or by auto-config/auto-encrypt). It is checked, along -// with the presence of an HTTPS port, to determine whether to enable TLS on -// incoming gRPC connections. +// GRPCServerUseTLS returns whether there's a TLS certificate configured for +// (external) gRPC (either manually or by auto-config/auto-encrypt), and use +// of TLS for gRPC has not been explicitly disabled at auto-encrypt. // // This function acquires a read lock because it reads from the config. -func (c *Configurator) GRPCTLSConfigured() bool { +func (c *Configurator) GRPCServerUseTLS() bool { c.lock.RLock() defer c.lock.RUnlock() - return c.grpc.cert != nil || c.autoTLS.cert != nil + return c.grpc.cert != nil || (c.grpc.useAutoCert && c.autoTLS.cert != nil) } // VerifyIncomingRPC returns true if we should verify incoming connnections to diff --git a/tlsutil/config_test.go b/tlsutil/config_test.go index 75fa839458..fc817aec69 100644 --- a/tlsutil/config_test.go +++ b/tlsutil/config_test.go @@ -1465,7 +1465,7 @@ func TestConfigurator_AuthorizeInternalRPCServerConn(t *testing.T) { }) } -func TestConfigurator_GRPCTLSConfigured(t *testing.T) { +func TestConfigurator_GRPCServerUseTLS(t *testing.T) { t.Run("certificate manually configured", func(t *testing.T) { c := makeConfigurator(t, Config{ GRPC: ProtocolConfig{ @@ -1473,22 +1473,47 @@ func TestConfigurator_GRPCTLSConfigured(t *testing.T) { KeyFile: "../test/hostname/Alice.key", }, }) - require.True(t, c.GRPCTLSConfigured()) + require.True(t, c.GRPCServerUseTLS()) }) - t.Run("AutoTLS", func(t *testing.T) { + t.Run("no certificate", func(t *testing.T) { + c := makeConfigurator(t, Config{}) + require.False(t, c.GRPCServerUseTLS()) + }) + + t.Run("AutoTLS (default)", func(t *testing.T) { c := makeConfigurator(t, Config{}) bobCert := loadFile(t, "../test/hostname/Bob.crt") bobKey := loadFile(t, "../test/hostname/Bob.key") require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey)) - - require.True(t, c.GRPCTLSConfigured()) + require.False(t, c.GRPCServerUseTLS()) }) - t.Run("no certificate", func(t *testing.T) { - c := makeConfigurator(t, Config{}) - require.False(t, c.GRPCTLSConfigured()) + t.Run("AutoTLS w/ UseAutoCert Disabled", func(t *testing.T) { + c := makeConfigurator(t, Config{ + GRPC: ProtocolConfig{ + UseAutoCert: false, + }, + }) + + bobCert := loadFile(t, "../test/hostname/Bob.crt") + bobKey := loadFile(t, "../test/hostname/Bob.key") + require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey)) + require.False(t, c.GRPCServerUseTLS()) + }) + + t.Run("AutoTLS w/ UseAutoCert Enabled", func(t *testing.T) { + c := makeConfigurator(t, Config{ + GRPC: ProtocolConfig{ + UseAutoCert: true, + }, + }) + + bobCert := loadFile(t, "../test/hostname/Bob.crt") + bobKey := loadFile(t, "../test/hostname/Bob.key") + require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey)) + require.True(t, c.GRPCServerUseTLS()) }) } diff --git a/website/content/docs/agent/config/config-files.mdx b/website/content/docs/agent/config/config-files.mdx index bf3e219bee..2631378731 100644 --- a/website/content/docs/agent/config/config-files.mdx +++ b/website/content/docs/agent/config/config-files.mdx @@ -2019,6 +2019,8 @@ specially crafted certificate signed by the CA can be used to gain full access t - `verify_incoming` - ((#tls_grpc_verify_incoming)) Overrides [`tls.defaults.verify_incoming`](#tls_defaults_verify_incoming). + - `use_auto_cert` - (Defaults to `false`) Enables or disables TLS on gRPC servers. Set to `true` to allow `auto_encrypt` TLS settings to apply to gRPC listeners. We recommend disabling TLS on gRPC servers if you are using `auto_encrypt` for other TLS purposes, such as enabling HTTPS. + - `https` ((#tls_https)) Provides settings for the HTTPS interface. To enable the HTTPS interface you must define a port via [`ports.https`](#https_port). From 919da333314df19faa8686b33a3348167e6baab3 Mon Sep 17 00:00:00 2001 From: skpratt Date: Wed, 24 Aug 2022 12:00:09 -0500 Subject: [PATCH 317/339] no-op: refactor usagemetrics tests for clarity and DRY cases (#14313) --- .../usagemetrics/usagemetrics_oss_test.go | 2067 +++++------------ 1 file changed, 560 insertions(+), 1507 deletions(-) diff --git a/agent/consul/usagemetrics/usagemetrics_oss_test.go b/agent/consul/usagemetrics/usagemetrics_oss_test.go index c860e5b741..8c37fe2695 100644 --- a/agent/consul/usagemetrics/usagemetrics_oss_test.go +++ b/agent/consul/usagemetrics/usagemetrics_oss_test.go @@ -8,10 +8,11 @@ import ( "time" "github.com/armon/go-metrics" - uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/serf/serf" "github.com/stretchr/testify/require" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" @@ -23,371 +24,368 @@ func newStateStore() (*state.Store, error) { return state.NewStateStore(nil), nil } +type testCase struct { + modfiyStateStore func(t *testing.T, s *state.Store) + getMembersFunc getMembersFunc + expectedGauges map[string]metrics.GaugeValue +} + +var baseCases = map[string]testCase{ + "empty-state": { + expectedGauges: map[string]metrics.GaugeValue{ + // --- node --- + "consul.usage.test.consul.state.nodes;datacenter=dc1": { + Name: "consul.usage.test.consul.state.nodes", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- peering --- + "consul.usage.test.consul.state.peerings;datacenter=dc1": { + Name: "consul.usage.test.consul.state.peerings", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- member --- + "consul.usage.test.consul.members.clients;datacenter=dc1": { + Name: "consul.usage.test.consul.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.members.servers;datacenter=dc1": { + Name: "consul.usage.test.consul.members.servers", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service --- + "consul.usage.test.consul.state.services;datacenter=dc1": { + Name: "consul.usage.test.consul.state.services", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.state.service_instances;datacenter=dc1": { + Name: "consul.usage.test.consul.state.service_instances", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service mesh --- + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + }, + // --- kv --- + "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { + Name: "consul.usage.test.consul.state.kv_entries", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- config entries --- + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-intentions"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-resolver"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-router"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-splitter"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "proxy-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "exported-services"}, + }, + }, + }, + getMembersFunc: func() []serf.Member { return []serf.Member{} }, + }, + "nodes": { + modfiyStateStore: func(t *testing.T, s *state.Store) { + require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) + require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) + }, + getMembersFunc: func() []serf.Member { + return []serf.Member{ + { + Name: "foo", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + { + Name: "bar", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + } + }, + expectedGauges: map[string]metrics.GaugeValue{ + // --- node --- + "consul.usage.test.consul.state.nodes;datacenter=dc1": { + Name: "consul.usage.test.consul.state.nodes", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- peering --- + "consul.usage.test.consul.state.peerings;datacenter=dc1": { + Name: "consul.usage.test.consul.state.peerings", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- member --- + "consul.usage.test.consul.members.servers;datacenter=dc1": { + Name: "consul.usage.test.consul.members.servers", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.members.clients;datacenter=dc1": { + Name: "consul.usage.test.consul.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service --- + "consul.usage.test.consul.state.services;datacenter=dc1": { + Name: "consul.usage.test.consul.state.services", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.state.service_instances;datacenter=dc1": { + Name: "consul.usage.test.consul.state.service_instances", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service mesh --- + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + }, + // --- kv --- + "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { + Name: "consul.usage.test.consul.state.kv_entries", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- config entries --- + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-intentions"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-resolver"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-router"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-splitter"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "proxy-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "exported-services"}, + }, + }, + }, + }, +} + func TestUsageReporter_emitNodeUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue - } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "nodes": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) - require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "baz", - Tags: map[string]string{"role": "node"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 3, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 1, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - }, - } + cases := baseCases for name, tcase := range cases { t.Run(name, func(t *testing.T) { @@ -426,371 +424,57 @@ func TestUsageReporter_emitNodeUsage_OSS(t *testing.T) { } func TestUsageReporter_emitPeeringUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue + cases := make(map[string]testCase) + for k, v := range baseCases { + eg := make(map[string]metrics.GaugeValue) + for k, v := range v.expectedGauges { + eg[k] = v + } + cases[k] = testCase{v.modfiyStateStore, v.getMembersFunc, eg} } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "peerings": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - id, err := uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(1, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "foo", ID: id}})) - id, err = uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(2, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "bar", ID: id}})) - id, err = uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(3, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "baz", ID: id}})) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 3, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - }, + peeringsCase := cases["nodes"] + peeringsCase.modfiyStateStore = func(t *testing.T, s *state.Store) { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + require.NoError(t, s.PeeringWrite(1, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "foo", ID: id}})) + id, err = uuid.GenerateUUID() + require.NoError(t, err) + require.NoError(t, s.PeeringWrite(2, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "bar", ID: id}})) + id, err = uuid.GenerateUUID() + require.NoError(t, err) + require.NoError(t, s.PeeringWrite(3, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "baz", ID: id}})) } + peeringsCase.getMembersFunc = func() []serf.Member { + return []serf.Member{ + { + Name: "foo", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + { + Name: "bar", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + } + } + peeringsCase.expectedGauges["consul.usage.test.consul.state.nodes;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.nodes", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.consul.state.peerings;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.peerings", + Value: 3, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.consul.members.clients;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + cases["peerings"] = peeringsCase + delete(cases, "nodes") for name, tcase := range cases { t.Run(name, func(t *testing.T) { @@ -829,420 +513,134 @@ func TestUsageReporter_emitPeeringUsage_OSS(t *testing.T) { } func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue + cases := make(map[string]testCase) + for k, v := range baseCases { + eg := make(map[string]metrics.GaugeValue) + for k, v := range v.expectedGauges { + eg[k] = v + } + cases[k] = testCase{v.modfiyStateStore, v.getMembersFunc, eg} } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "nodes-and-services": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) - require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(4, &structs.Node{Node: "qux", Address: "127.0.0.3"})) - mgw := structs.TestNodeServiceMeshGateway(t) - mgw.ID = "mesh-gateway" + nodesAndSvcsCase := cases["nodes"] + nodesAndSvcsCase.modfiyStateStore = func(t *testing.T, s *state.Store) { + require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) + require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) + require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) + require.NoError(t, s.EnsureNode(4, &structs.Node{Node: "qux", Address: "127.0.0.3"})) - tgw := structs.TestNodeServiceTerminatingGateway(t, "1.1.1.1") - tgw.ID = "terminating-gateway" - // Typical services and some consul services spread across two nodes - require.NoError(t, s.EnsureService(5, "foo", &structs.NodeService{ID: "db", Service: "db", Tags: nil, Address: "", Port: 5000})) - require.NoError(t, s.EnsureService(6, "bar", &structs.NodeService{ID: "api", Service: "api", Tags: nil, Address: "", Port: 5000})) - require.NoError(t, s.EnsureService(7, "foo", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) - require.NoError(t, s.EnsureService(8, "bar", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) - require.NoError(t, s.EnsureService(9, "foo", &structs.NodeService{ID: "db-connect-proxy", Service: "db-connect-proxy", Tags: nil, Address: "", Port: 5000, Kind: structs.ServiceKindConnectProxy})) - require.NoError(t, s.EnsureRegistration(10, structs.TestRegisterIngressGateway(t))) - require.NoError(t, s.EnsureService(11, "foo", mgw)) - require.NoError(t, s.EnsureService(12, "foo", tgw)) - require.NoError(t, s.EnsureService(13, "bar", &structs.NodeService{ID: "db-native", Service: "db", Tags: nil, Address: "", Port: 5000, Connect: structs.ServiceConnect{Native: true}})) - require.NoError(t, s.EnsureConfigEntry(14, &structs.IngressGatewayConfigEntry{ - Kind: structs.IngressGateway, - Name: "foo", - })) - require.NoError(t, s.EnsureConfigEntry(15, &structs.IngressGatewayConfigEntry{ - Kind: structs.IngressGateway, - Name: "bar", - })) - require.NoError(t, s.EnsureConfigEntry(16, &structs.IngressGatewayConfigEntry{ - Kind: structs.IngressGateway, - Name: "baz", - })) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "baz", - Tags: map[string]string{"role": "node", "segment": "a"}, - Status: serf.StatusAlive, - }, - { - Name: "qux", - Tags: map[string]string{"role": "node", "segment": "b"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 4, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 2, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 7, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 9, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 3, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, + mgw := structs.TestNodeServiceMeshGateway(t) + mgw.ID = "mesh-gateway" + + tgw := structs.TestNodeServiceTerminatingGateway(t, "1.1.1.1") + tgw.ID = "terminating-gateway" + // Typical services and some consul services spread across two nodes + require.NoError(t, s.EnsureService(5, "foo", &structs.NodeService{ID: "db", Service: "db", Tags: nil, Address: "", Port: 5000})) + require.NoError(t, s.EnsureService(6, "bar", &structs.NodeService{ID: "api", Service: "api", Tags: nil, Address: "", Port: 5000})) + require.NoError(t, s.EnsureService(7, "foo", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) + require.NoError(t, s.EnsureService(8, "bar", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) + require.NoError(t, s.EnsureService(9, "foo", &structs.NodeService{ID: "db-connect-proxy", Service: "db-connect-proxy", Tags: nil, Address: "", Port: 5000, Kind: structs.ServiceKindConnectProxy})) + require.NoError(t, s.EnsureRegistration(10, structs.TestRegisterIngressGateway(t))) + require.NoError(t, s.EnsureService(11, "foo", mgw)) + require.NoError(t, s.EnsureService(12, "foo", tgw)) + require.NoError(t, s.EnsureService(13, "bar", &structs.NodeService{ID: "db-native", Service: "db", Tags: nil, Address: "", Port: 5000, Connect: structs.ServiceConnect{Native: true}})) + require.NoError(t, s.EnsureConfigEntry(14, &structs.IngressGatewayConfigEntry{ + Kind: structs.IngressGateway, + Name: "foo", + })) + require.NoError(t, s.EnsureConfigEntry(15, &structs.IngressGatewayConfigEntry{ + Kind: structs.IngressGateway, + Name: "bar", + })) + require.NoError(t, s.EnsureConfigEntry(16, &structs.IngressGatewayConfigEntry{ + Kind: structs.IngressGateway, + Name: "baz", + })) + } + baseCaseMembers := nodesAndSvcsCase.getMembersFunc() + nodesAndSvcsCase.getMembersFunc = func() []serf.Member { + baseCaseMembers = append(baseCaseMembers, serf.Member{ + Name: "baz", + Tags: map[string]string{"role": "node", "segment": "a"}, + Status: serf.StatusAlive, + }) + baseCaseMembers = append(baseCaseMembers, serf.Member{ + Name: "qux", + Tags: map[string]string{"role": "node", "segment": "b"}, + Status: serf.StatusAlive, + }) + return baseCaseMembers + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.nodes;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.nodes", + Value: 4, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.members.clients;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.members.clients", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.services;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.services", + Value: 7, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.service_instances;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.service_instances", + Value: 9, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, }, } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.config_entries", + Value: 3, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + } + cases["nodes-and-services"] = nodesAndSvcsCase + delete(cases, "nodes") for name, tcase := range cases { t.Run(name, func(t *testing.T) { @@ -1280,379 +678,34 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) { } func TestUsageReporter_emitKVUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue + cases := make(map[string]testCase) + for k, v := range baseCases { + eg := make(map[string]metrics.GaugeValue) + for k, v := range v.expectedGauges { + eg[k] = v + } + cases[k] = testCase{v.modfiyStateStore, v.getMembersFunc, eg} } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "nodes": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) - require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) - require.NoError(t, s.KVSSet(4, &structs.DirEntry{Key: "a", Value: []byte{1}})) - require.NoError(t, s.KVSSet(5, &structs.DirEntry{Key: "b", Value: []byte{1}})) - require.NoError(t, s.KVSSet(6, &structs.DirEntry{Key: "c", Value: []byte{1}})) - require.NoError(t, s.KVSSet(7, &structs.DirEntry{Key: "d", Value: []byte{1}})) - require.NoError(t, s.KVSDelete(8, "d", &acl.EnterpriseMeta{})) - require.NoError(t, s.KVSDelete(9, "c", &acl.EnterpriseMeta{})) - require.NoError(t, s.KVSSet(10, &structs.DirEntry{Key: "e", Value: []byte{1}})) - require.NoError(t, s.KVSSet(11, &structs.DirEntry{Key: "f", Value: []byte{1}})) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "baz", - Tags: map[string]string{"role": "node"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 3, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 1, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 4, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - }, + nodesCase := cases["nodes"] + mss := nodesCase.modfiyStateStore + nodesCase.modfiyStateStore = func(t *testing.T, s *state.Store) { + mss(t, s) + require.NoError(t, s.KVSSet(4, &structs.DirEntry{Key: "a", Value: []byte{1}})) + require.NoError(t, s.KVSSet(5, &structs.DirEntry{Key: "b", Value: []byte{1}})) + require.NoError(t, s.KVSSet(6, &structs.DirEntry{Key: "c", Value: []byte{1}})) + require.NoError(t, s.KVSSet(7, &structs.DirEntry{Key: "d", Value: []byte{1}})) + require.NoError(t, s.KVSDelete(8, "d", &acl.EnterpriseMeta{})) + require.NoError(t, s.KVSDelete(9, "c", &acl.EnterpriseMeta{})) + require.NoError(t, s.KVSSet(10, &structs.DirEntry{Key: "e", Value: []byte{1}})) + require.NoError(t, s.KVSSet(11, &structs.DirEntry{Key: "f", Value: []byte{1}})) } + nodesCase.expectedGauges["consul.usage.test.consul.state.kv_entries;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.consul.state.kv_entries", + Value: 4, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + cases["nodes"] = nodesCase for name, tcase := range cases { t.Run(name, func(t *testing.T) { From 8f27a077cbd5b27edbeae962bcdf83ac30776059 Mon Sep 17 00:00:00 2001 From: Derek Menteer Date: Wed, 24 Aug 2022 12:39:15 -0500 Subject: [PATCH 318/339] Add 14269 changelog entry. --- .changelog/14269.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/14269.txt diff --git a/.changelog/14269.txt b/.changelog/14269.txt new file mode 100644 index 0000000000..29eec6d5da --- /dev/null +++ b/.changelog/14269.txt @@ -0,0 +1,3 @@ +```release-note:bugfix +connect: Fix issue where `auto_config` and `auto_encrypt` could unintentionally enable TLS for gRPC xDS connections. +``` \ No newline at end of file From 41aea6521496d8a86a7cef842660997348e345c0 Mon Sep 17 00:00:00 2001 From: cskh Date: Wed, 24 Aug 2022 14:13:10 -0400 Subject: [PATCH 319/339] =?UTF-8?q?Fix:=20the=20inboundconnection=20limit?= =?UTF-8?q?=20filter=20should=20be=20placed=20in=20front=20of=20http=20co?= =?UTF-8?q?=E2=80=A6=20(#14325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: the inboundconnection limit should be placed in front of http connection manager Co-authored-by: Freddy --- agent/xds/listeners.go | 44 ++++++++++++------- ...ener-max-inbound-connections.latest.golden | 15 ++++--- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index 95b84c94ce..33c339c4d8 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -1214,16 +1214,38 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot filterOpts.forwardClientPolicy = envoy_http_v3.HttpConnectionManager_APPEND_FORWARD } } + + // If an inbound connect limit is set, inject a connection limit filter on each chain. + if cfg.MaxInboundConnections > 0 { + connectionLimitFilter, err := makeConnectionLimitFilter(cfg.MaxInboundConnections) + if err != nil { + return nil, err + } + l.FilterChains = []*envoy_listener_v3.FilterChain{ + { + Filters: []*envoy_listener_v3.Filter{ + connectionLimitFilter, + }, + }, + } + } + filter, err := makeListenerFilter(filterOpts) if err != nil { return nil, err } - l.FilterChains = []*envoy_listener_v3.FilterChain{ - { - Filters: []*envoy_listener_v3.Filter{ - filter, + + if len(l.FilterChains) > 0 { + // The list of FilterChains has already been initialized + l.FilterChains[0].Filters = append(l.FilterChains[0].Filters, filter) + } else { + l.FilterChains = []*envoy_listener_v3.FilterChain{ + { + Filters: []*envoy_listener_v3.Filter{ + filter, + }, }, - }, + } } err = s.finalizePublicListenerFromConfig(l, cfgSnap, cfg, useHTTPFilter) @@ -1249,17 +1271,6 @@ func (s *ResourceGenerator) finalizePublicListenerFromConfig(l *envoy_listener_v return nil } - // If an inbound connect limit is set, inject a connection limit filter on each chain. - if proxyCfg.MaxInboundConnections > 0 { - filter, err := makeConnectionLimitFilter(proxyCfg.MaxInboundConnections) - if err != nil { - return nil - } - for idx := range l.FilterChains { - l.FilterChains[idx].Filters = append(l.FilterChains[idx].Filters, filter) - } - } - return nil } @@ -1990,6 +2001,7 @@ func makeTCPProxyFilter(filterName, cluster, statPrefix string) (*envoy_listener func makeConnectionLimitFilter(limit int) (*envoy_listener_v3.Filter, error) { cfg := &envoy_connection_limit_v3.ConnectionLimit{ + StatPrefix: "inbound_connection_limit", MaxConnections: wrapperspb.UInt64(uint64(limit)), } return makeFilter("envoy.filters.network.connection_limit", cfg) diff --git a/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden b/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden index be3b83433a..cbfda69f56 100644 --- a/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden +++ b/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden @@ -73,6 +73,14 @@ "statPrefix": "connect_authz" } }, + { + "name": "envoy.filters.network.connection_limit", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.connection_limit.v3.ConnectionLimit", + "statPrefix": "inbound_connection_limit", + "maxConnections": "222" + } + }, { "name": "envoy.filters.network.tcp_proxy", "typedConfig": { @@ -80,13 +88,6 @@ "statPrefix": "public_listener", "cluster": "local_app" } - }, - { - "name": "envoy.filters.network.connection_limit", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.connection_limit.v3.ConnectionLimit", - "maxConnections": "222" - } } ], "transportSocket": { From 8e6b6a49a2d473a8cc52cab2cd55a21073ee14e2 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Wed, 24 Aug 2022 17:04:26 -0700 Subject: [PATCH 320/339] docs: Update Envoy support matrix to match the code (#14338) --- website/content/docs/connect/proxies/envoy.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/connect/proxies/envoy.mdx b/website/content/docs/connect/proxies/envoy.mdx index 526d642bc8..7ada5b6fd0 100644 --- a/website/content/docs/connect/proxies/envoy.mdx +++ b/website/content/docs/connect/proxies/envoy.mdx @@ -37,8 +37,8 @@ Consul supports **four major Envoy releases** at the beginning of each major Con | Consul Version | Compatible Envoy Versions | | ------------------- | -----------------------------------------------------------------------------------| | 1.13.x | 1.23.0, 1.22.2, 1.21.4, 1.20.6 | -| 1.12.x | 1.22.2, 1.21.3, 1.20.4, 1.19.5 | -| 1.11.x | 1.20.2, 1.19.3, 1.18.6, 1.17.41 | +| 1.12.x | 1.22.2, 1.21.4, 1.20.6, 1.19.5 | +| 1.11.x | 1.20.6, 1.19.5, 1.18.6, 1.17.41 | 1. Envoy 1.20.1 and earlier are vulnerable to [CVE-2022-21654](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21654) and [CVE-2022-21655](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21655). Both CVEs were patched in Envoy versions 1.18.6, 1.19.3, and 1.20.2. Envoy 1.16.x and older releases are no longer supported (see [HCSEC-2022-07](https://discuss.hashicorp.com/t/hcsec-2022-07-consul-s-connect-service-mesh-affected-by-recent-envoy-security-releases/36332)). Consul 1.9.x clusters should be upgraded to 1.10.x and Envoy upgraded to the latest supported Envoy version for that release, 1.18.6. From 181063cd2399a8cf1243e489a9f345b0a91e7fa5 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Thu, 25 Aug 2022 11:25:59 -0400 Subject: [PATCH 321/339] Exit loop when context is cancelled --- agent/consul/leader_peering.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agent/consul/leader_peering.go b/agent/consul/leader_peering.go index bc5b669cdd..d1823b026b 100644 --- a/agent/consul/leader_peering.go +++ b/agent/consul/leader_peering.go @@ -391,6 +391,12 @@ func (s *Server) runPeeringDeletions(ctx context.Context) error { // process. This includes deletion of the peerings themselves in addition to any peering data raftLimiter := rate.NewLimiter(defaultDeletionApplyRate, int(defaultDeletionApplyRate)) for { + select { + case <-ctx.Done(): + return nil + default: + } + ws := memdb.NewWatchSet() state := s.fsm.State() _, peerings, err := s.fsm.State().PeeringListDeleted(ws) From 4d40d02c73d8daeff833f451e9e1714e6d0a7cde Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 12:45:57 -0400 Subject: [PATCH 322/339] Remove warning about 1.9 --- website/content/docs/k8s/connect/terminating-gateways.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index e82bd773fb..9ff16b5c64 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -6,8 +6,6 @@ description: Configuring Terminating Gateways on Kubernetes # Terminating Gateways on Kubernetes --> 1.9.0+: This feature is available in Consul versions 1.9.0 and higher - ~> This topic requires familiarity with [Terminating Gateways](/docs/connect/gateways/terminating-gateway). Adding a terminating gateway is a multi-step process: From ac129339f8ec9e3081b915c9e4cf4a575f23dad2 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 12:49:54 -0400 Subject: [PATCH 323/339] Instruct users to use the CLI --- website/content/docs/k8s/connect/terminating-gateways.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 9ff16b5c64..c5607d4380 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -36,9 +36,11 @@ terminatingGateways: ## Deploying the Helm chart -Ensure you have the latest consul-helm chart and install Consul via helm using the following -[guide](/docs/k8s/installation/install#installing-consul) while being sure to provide the yaml configuration -as previously discussed. +The Helm chart may be deployed using the [Consul on Kubernetes CLI](/docs/k8s/k8s-cli). + +```shell-session +consul-k8s install -f config.yaml +``` ## Accessing the Consul agent From 884dda25c26caced01ae4a00ca89f5a277acf26e Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 13:02:55 -0400 Subject: [PATCH 324/339] Use tabs for with and without TLS --- .../docs/k8s/connect/terminating-gateways.mdx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index c5607d4380..9fb2b149c4 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -6,8 +6,6 @@ description: Configuring Terminating Gateways on Kubernetes # Terminating Gateways on Kubernetes -~> This topic requires familiarity with [Terminating Gateways](/docs/connect/gateways/terminating-gateway). - Adding a terminating gateway is a multi-step process: - Update the Helm chart with terminating gateway config options @@ -15,6 +13,12 @@ Adding a terminating gateway is a multi-step process: - Access the Consul agent - Register external services with Consul +## Requirements + +- [Consul]() +- [Consul on Kubernetes CLI]() +- Familiarity with [Terminating Gateways](/docs/connect/gateways/terminating-gateway) + ## Update the helm chart with terminating gateway config options Minimum required Helm options: @@ -39,36 +43,38 @@ terminatingGateways: The Helm chart may be deployed using the [Consul on Kubernetes CLI](/docs/k8s/k8s-cli). ```shell-session -consul-k8s install -f config.yaml +$ consul-k8s install -f config.yaml ``` ## Accessing the Consul agent -You can access the Consul server directly from your host via `kubectl port-forward`. This is helpful for interacting with your Consul UI locally as well as to validate connectivity of the application. +You can access the Consul server directly from your host via `kubectl port-forward`. This is helpful for interacting with your Consul UI locally as well as for validating the connectivity of the application. + + + ```shell-session $ kubectl port-forward consul-server-0 8500 & ``` +```shell-session +$ export CONSUL_HTTP_ADDR=http://localhost:8500 +``` + + + If TLS is enabled use port 8501: ```shell-session $ kubectl port-forward consul-server-0 8501 & ``` --> Be sure the latest consul binary is installed locally on your host. -[https://releases.hashicorp.com/consul/](https://releases.hashicorp.com/consul/) - -```shell-session -$ export CONSUL_HTTP_ADDR=http://localhost:8500 -``` - -If TLS is enabled set: - ```shell-session $ export CONSUL_HTTP_ADDR=https://localhost:8501 $ export CONSUL_HTTP_SSL_VERIFY=false ``` + + If ACLs are enabled also set: From 65dce3476f5e5f3d1a6e55b8e2448c8b525071e8 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 13:27:43 -0400 Subject: [PATCH 325/339] Clean up copy for registration --- .../docs/k8s/connect/terminating-gateways.mdx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 9fb2b149c4..188da110f0 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -94,14 +94,15 @@ Registering the external services with Consul is a multi-step process: ### Register external services with Consul -There are two ways to register an external service with Consul: -1. If [`TransparentProxy`](/docs/connect/transparent-proxy) is enabled, the preferred method is to declare external endpoints in the [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of `ServiceDefaults`. -1. You can add the service as a node in the Consul catalog. +You may register an external service with Consul using `ServiceDefaults` if +[`TransparentProxy`](/docs/connect/transparent-proxy) is enabled. Otherwise, +you may register the service as a node in the Consul catalog. -#### Register an external service as a destination + + -The [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of the `ServiceDefaults` Custom Resource Definition (CRD) allows clients to dial the external service directly. It is valid only in [`TransparentProxy`](/docs/connect/transparent-proxy)) mode. -The following table describes traffic behaviors when using `destination`s to route traffic through a terminating gateway: +The [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of the `ServiceDefaults` Custom Resource Definition (CRD) allows clients to dial an external service directly. For this method to work, [`TransparentProxy`](/docs/connect/transparent-proxy) must be enabled. +The following table describes traffic behaviors when using the `destination` field to route traffic through a terminating gateway: | External Services Layer | Client dials | Client uses TLS | Allowed | Notes | |---|---|---|---|---| @@ -145,8 +146,8 @@ $ kubectl apply --filename serviceDefaults.yaml ``` All other terminating gateway operations can use the name of the `ServiceDefaults` in place of a typical Consul service name. - -#### Register an external service as a Catalog Node + + -> **Note:** Normal Consul services are registered with the Consul client on the node that they're running on. Since this is an external service, there is no Consul node @@ -197,6 +198,10 @@ If ACLs and TLS are enabled : $ curl --request PUT --header "X-Consul-Token: $CONSUL_HTTP_TOKEN" --data @external.json --insecure $CONSUL_HTTP_ADDR/v1/catalog/register true ``` + + + + ### Update terminating gateway ACL role if ACLs are enabled From a2a7b56292b0f64d3e94d3fbc72562420c4589b4 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 13:37:52 -0400 Subject: [PATCH 326/339] Format traffic behaviors table --- .../docs/k8s/connect/terminating-gateways.mdx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 188da110f0..c685c07c20 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -104,16 +104,16 @@ you may register the service as a node in the Consul catalog. The [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of the `ServiceDefaults` Custom Resource Definition (CRD) allows clients to dial an external service directly. For this method to work, [`TransparentProxy`](/docs/connect/transparent-proxy) must be enabled. The following table describes traffic behaviors when using the `destination` field to route traffic through a terminating gateway: -| External Services Layer | Client dials | Client uses TLS | Allowed | Notes | -|---|---|---|---|---| -| L4 | Hostname | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | -| L4 | IP | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | -| L4 | Hostname | No | Not allowed | The sidecar is not protocol aware and can not identify traffic going to the external service. | -| L4 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | -| L7 | Hostname | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | -| L7 | IP | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | -| L7 | Hostname | No | Allowed | A `Host` or `:authority` header is required. | -| L7 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | +| External Services Layer | Client dials | Client uses TLS | Allowed | Notes | +| ----------------------- | ------------ | --------------- | ----------- | --------------------------------------------------------------------------------------------- | +| L4 | Hostname | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | +| L4 | IP | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | +| L4 | Hostname | No | Not allowed | The sidecar is not protocol aware and can not identify traffic going to the external service. | +| L4 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | +| L7 | Hostname | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | +| L7 | IP | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | +| L7 | Hostname | No | Allowed | A `Host` or `:authority` header is required. | +| L7 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | You can provide a `caFile` to secure traffic between unencrypted clients that connect to external services through the terminating gateway. Refer to [Create the configuration entry for the terminating gateway](#create-the-configuration-entry-for-the-terminating-gateway) for details. From e990b03d5cd0af5136703070694a08ce6d8faa42 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 13:56:13 -0400 Subject: [PATCH 327/339] Normalize table with nobrs --- .../docs/k8s/connect/terminating-gateways.mdx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index c685c07c20..80654698a3 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -104,25 +104,25 @@ you may register the service as a node in the Consul catalog. The [`destination`](/docs/connect/config-entries/service-defaults#terminating-gateway-destination) field of the `ServiceDefaults` Custom Resource Definition (CRD) allows clients to dial an external service directly. For this method to work, [`TransparentProxy`](/docs/connect/transparent-proxy) must be enabled. The following table describes traffic behaviors when using the `destination` field to route traffic through a terminating gateway: -| External Services Layer | Client dials | Client uses TLS | Allowed | Notes | -| ----------------------- | ------------ | --------------- | ----------- | --------------------------------------------------------------------------------------------- | -| L4 | Hostname | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | -| L4 | IP | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | -| L4 | Hostname | No | Not allowed | The sidecar is not protocol aware and can not identify traffic going to the external service. | -| L4 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | -| L7 | Hostname | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | -| L7 | IP | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | -| L7 | Hostname | No | Allowed | A `Host` or `:authority` header is required. | -| L7 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | +| External Services Layer | Client dials | Client uses TLS | Allowed | Notes | +|--------------------------------------|---------------------------|------------------------------|--------------------------|-----------------------------------------------------------------------------------------------| +| L4 | Hostname | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | +| L4 | IP | Yes | Allowed | `CAFiles` are not allowed because traffic is already end-to-end encrypted by the client. | +| L4 | Hostname | No | Not allowed | The sidecar is not protocol aware and can not identify traffic going to the external service. | +| L4 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | +| L7 | Hostname | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | +| L7 | IP | Yes | Not allowed | Because traffic is already encrypted before the sidecar, it cannot route as L7 traffic. | +| L7 | Hostname | No | Allowed | A `Host` or `:authority` header is required. | +| L7 | IP | No | Allowed | There are no limitations on dialing IPs without TLS. | You can provide a `caFile` to secure traffic between unencrypted clients that connect to external services through the terminating gateway. Refer to [Create the configuration entry for the terminating gateway](#create-the-configuration-entry-for-the-terminating-gateway) for details. -Also note that regardless of the `protocol` specified in the `ServiceDefaults`, [L7 intentions](/docs/connect/config-entries/service-intentions#permissions) are not currently supported with `ServiceDefaults` destinations. +-> **Note:** Regardless of the `protocol` specified in the `ServiceDefaults`, [L7 intentions](/docs/connect/config-entries/service-intentions#permissions) are not currently supported with `ServiceDefaults` destinations. Create a `ServiceDefaults` custom resource for the external service: - + ```yaml apiVersion: consul.hashicorp.com/v1alpha1 @@ -142,14 +142,15 @@ Create a `ServiceDefaults` custom resource for the external service: Apply the `ServiceDefaults` resource with `kubectl apply`: ```shell-session -$ kubectl apply --filename serviceDefaults.yaml +$ kubectl apply --filename service-defaults.yaml ``` -All other terminating gateway operations can use the name of the `ServiceDefaults` in place of a typical Consul service name. +All other terminating gateway operations can use the name of the `ServiceDefaults` component, in this case "example-https", as a Consul service name. + --> **Note:** Normal Consul services are registered with the Consul client on the node that +Normally, Consul services are registered with the Consul client on the node that they're running on. Since this is an external service, there is no Consul node to register it onto. Instead, we will make up a node name and register the service to that node. From 6d9872388bcb4dc7a9b3f8f6cde67a70731f0037 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 14:03:43 -0400 Subject: [PATCH 328/339] Clean up copy in ACL role update --- .../content/docs/k8s/connect/terminating-gateways.mdx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 80654698a3..9ed9865a3a 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -202,15 +202,12 @@ true - - ### Update terminating gateway ACL role if ACLs are enabled If ACLs are enabled, update the terminating gateway acl role to have `service: write` permissions on all of the services -being represented by the gateway: +being represented by the gateway. -- Create a new policy that includes these permissions -- Update the existing role to include the new policy +Create a new policy that includes the write permission for the service you created. @@ -242,7 +239,7 @@ consul acl role list | grep -B 6 -- "- RELEASE_NAME-terminating-gateway-policy" ID: ``` -Update the terminating gateway acl token with the new policy +Update the terminating gateway ACL token with the new policy. ```shell-session $ consul acl role update -id -policy-name example-https-write-policy From 77c9995a8e1634c6bcb72e6f65e000f2c6252e5b Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 14:04:33 -0400 Subject: [PATCH 329/339] Lil' more cleanup --- website/content/docs/k8s/connect/terminating-gateways.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 9ed9865a3a..01937f6aa4 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -231,7 +231,7 @@ service "example-https" { } ``` -Now fetch the ID of the terminating gateway token +Fetch the ID of the terminating gateway token. ```shell-session consul acl role list | grep -B 6 -- "- RELEASE_NAME-terminating-gateway-policy" | grep ID From ed4a430b3ec17d7d70b671fb4e01deab63fbc8a4 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 14:40:18 -0400 Subject: [PATCH 330/339] Use tabs for destinations --- .../docs/k8s/connect/terminating-gateways.mdx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 01937f6aa4..2119e54e4f 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -278,8 +278,6 @@ Configure the [`caFile`](https://www.consul.io/docs/connect/config-entries/termi - Consul Helm chart 0.43 or older - An Envoy image with an alpine base image -For `ServiceDefaults` destinations, refer to [Register an external service as a destination](#register-an-external-service-as-a-destination). - Apply the `TerminatingGateway` resource with `kubectl apply`: ```shell-session @@ -315,7 +313,7 @@ $ kubectl apply --filename service-intentions.yaml ### Define the external services as upstreams for services in the mesh -Finally define and deploy the external services as upstreams for the internal mesh services that wish to talk to them. +As a final step, you may define and deploy the external services as upstreams for the internal mesh services that wish to talk to them. An example deployment is provided which will serve as a static client for the terminating gateway service. @@ -364,33 +362,35 @@ spec: -Run the service via `kubectl apply`: +Deploy the service with `kubectl apply`. ```shell-session $ kubectl apply --filename static-client.yaml ``` -Wait for the service to be ready: +Wait for the service to be ready. ```shell-session $ kubectl rollout status deploy static-client --watch deployment "static-client" successfully rolled out ``` -You can verify connectivity of the static-client and terminating gateway via a curl command: +You can verify connectivity of the static-client and terminating gateway via a curl command. - - -```shell-session -$ kubectl exec deploy/static-client -- curl -vvvs --header "Host: example-https.com" http://localhost:1234/ -``` - - - - + + ```shell-session $ kubectl exec deploy/static-client -- curl -vvvs https://example.com/ ``` - + + + +```shell-session +$ kubectl exec deploy/static-client -- curl -vvvs --header "Host: example-https.com" http://localhost:1234/ +``` + + + + From 5064fbc2545706dec7c4a8b87bf7a7f9ba05e354 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 14:44:33 -0400 Subject: [PATCH 331/339] Add links to requirements --- website/content/docs/k8s/connect/terminating-gateways.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index 2119e54e4f..ed1b11e8c7 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -15,8 +15,8 @@ Adding a terminating gateway is a multi-step process: ## Requirements -- [Consul]() -- [Consul on Kubernetes CLI]() +- [Consul](https://www.consul.io/docs/install#install-consul) +- [Consul on Kubernetes CLI](/docs/k8s/k8s-cli) - Familiarity with [Terminating Gateways](/docs/connect/gateways/terminating-gateway) ## Update the helm chart with terminating gateway config options From 70a1cbd8ea3a1efef2373cb15c4657d590c31615 Mon Sep 17 00:00:00 2001 From: Thomas Eckert Date: Thu, 25 Aug 2022 14:44:45 -0400 Subject: [PATCH 332/339] Capitalize Helm --- website/content/docs/k8s/connect/terminating-gateways.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/k8s/connect/terminating-gateways.mdx b/website/content/docs/k8s/connect/terminating-gateways.mdx index ed1b11e8c7..06316f5f51 100644 --- a/website/content/docs/k8s/connect/terminating-gateways.mdx +++ b/website/content/docs/k8s/connect/terminating-gateways.mdx @@ -19,7 +19,7 @@ Adding a terminating gateway is a multi-step process: - [Consul on Kubernetes CLI](/docs/k8s/k8s-cli) - Familiarity with [Terminating Gateways](/docs/connect/gateways/terminating-gateway) -## Update the helm chart with terminating gateway config options +## Update the Helm chart with terminating gateway config options Minimum required Helm options: From 20f291fa066efb58ed99b1972934807d7b8605ef Mon Sep 17 00:00:00 2001 From: Jared Kirschner Date: Wed, 27 Jul 2022 14:03:06 -0700 Subject: [PATCH 333/339] docs: improve health check related docs Includes: - Improved scannability and organization of checks overview - Checks overview includes more guidance on - How to register a health check - The options available for a health check definition - Contextual cross-references to maintenance mode --- website/content/api-docs/agent/check.mdx | 13 +- website/content/api-docs/health.mdx | 3 + website/content/docs/discovery/checks.mdx | 595 +++++++++++++--------- 3 files changed, 365 insertions(+), 246 deletions(-) diff --git a/website/content/api-docs/agent/check.mdx b/website/content/api-docs/agent/check.mdx index eafbb17c42..785fbce8b3 100644 --- a/website/content/api-docs/agent/check.mdx +++ b/website/content/api-docs/agent/check.mdx @@ -6,7 +6,10 @@ description: The /agent/check endpoints interact with checks on the local agent # Check - Agent HTTP API -The `/agent/check` endpoints interact with checks on the local agent in Consul. +Consul's health check capabilities are described in the +[health checks overview](/docs/discovery/checks). +The `/agent/check` endpoints interact with health checks +managed by the local agent in Consul. These should not be confused with checks in the catalog. ## List Checks @@ -418,6 +421,10 @@ $ curl \ This endpoint is used with a TTL type check to set the status of the check to `critical` and to reset the TTL clock. +If you want to manually mark a service as unhealthy, +use [maintenance mode](/api-docs/agent#enable-maintenance-mode) +instead of defining a TTL health check and using this endpoint. + | Method | Path | Produces | | ------ | ----------------------------- | ------------------ | | `PUT` | `/agent/check/fail/:check_id` | `application/json` | @@ -456,6 +463,10 @@ $ curl \ This endpoint is used with a TTL type check to set the status of the check and to reset the TTL clock. +If you want to manually mark a service as unhealthy, +use [maintenance mode](/api-docs/agent#enable-maintenance-mode) +instead of defining a TTL health check and using this endpoint. + | Method | Path | Produces | | ------ | ------------------------------- | ------------------ | | `PUT` | `/agent/check/update/:check_id` | `application/json` | diff --git a/website/content/api-docs/health.mdx b/website/content/api-docs/health.mdx index 898c8ffe41..cad74bbad2 100644 --- a/website/content/api-docs/health.mdx +++ b/website/content/api-docs/health.mdx @@ -14,6 +14,9 @@ optional health checking mechanisms. Additionally, some of the query results from the health endpoints are filtered while the catalog endpoints provide the raw entries. +To modify health check registration or information, +use the [`/agent/check`](/api-docs/agent/check) endpoints. + ## List Checks for Node This endpoint returns the checks specific to the node provided on the path. diff --git a/website/content/docs/discovery/checks.mdx b/website/content/docs/discovery/checks.mdx index 5a21495793..1b4c4faf4b 100644 --- a/website/content/docs/discovery/checks.mdx +++ b/website/content/docs/discovery/checks.mdx @@ -13,144 +13,72 @@ description: >- One of the primary roles of the agent is management of system-level and application-level health checks. A health check is considered to be application-level if it is associated with a service. If not associated with a service, the check monitors the health of the entire node. -Review the [health checks tutorial](https://learn.hashicorp.com/tutorials/consul/service-registration-health-checks) to get a more complete example on how to leverage health check capabilities in Consul. -A check is defined in a configuration file or added at runtime over the HTTP interface. Checks -created via the HTTP interface persist with that node. +Review the [service health checks tutorial](https://learn.hashicorp.com/tutorials/consul/service-registration-health-checks) +to get a more complete example on how to leverage health check capabilities in Consul. -There are several different kinds of checks: +## Registering a health check -- Script + Interval - These checks depend on invoking an external application - that performs the health check, exits with an appropriate exit code, and potentially - generates some output. A script is paired with an invocation interval (e.g. - every 30 seconds). This is similar to the Nagios plugin system. The output of - a script check is limited to 4KB. Output larger than this will be truncated. - By default, Script checks will be configured with a timeout equal to 30 seconds. - It is possible to configure a custom Script check timeout value by specifying the - `timeout` field in the check definition. When the timeout is reached on Windows, - Consul will wait for any child processes spawned by the script to finish. For any - other system, Consul will attempt to force-kill the script and any child processes - it has spawned once the timeout has passed. - In Consul 0.9.0 and later, script checks are not enabled by default. To use them you - can either use : +There are three ways to register a service with health checks: - - [`enable_local_script_checks`](/docs/agent/config/cli-flags#_enable_local_script_checks): - enable script checks defined in local config files. Script checks defined via the HTTP - API will not be allowed. - - [`enable_script_checks`](/docs/agent/config/cli-flags#_enable_script_checks): enable - script checks regardless of how they are defined. +1. Start or reload a Consul agent with a service definition file in the + [agent's configuration directory](/docs/agent#configuring-consul-agents). +1. Call the + [`/agent/service/register`](/api-docs/agent/service#register-service) + HTTP API endpoint to register the service. +1. Use the + [`consul services register`](/commands/services/register) + CLI command to register the service. - ~> **Security Warning:** Enabling script checks in some configurations may - introduce a remote execution vulnerability which is known to be targeted by - malware. We strongly recommend `enable_local_script_checks` instead. See [this - blog post](https://www.hashicorp.com/blog/protecting-consul-from-rce-risk-in-specific-configurations) - for more details. +When a service is registered using the HTTP API endpoint or CLI command, +the checks persist in the Consul data folder across Consul agent restarts. -- `HTTP + Interval` - These checks make an HTTP `GET` request to the specified URL, - waiting the specified `interval` amount of time between requests (eg. 30 seconds). - The status of the service depends on the HTTP response code: any `2xx` code is - considered passing, a `429 Too ManyRequests` is a warning, and anything else is - a failure. This type of check - should be preferred over a script that uses `curl` or another external process - to check a simple HTTP operation. By default, HTTP checks are `GET` requests - unless the `method` field specifies a different method. Additional header - fields can be set through the `header` field which is a map of lists of - strings, e.g. `{"x-foo": ["bar", "baz"]}`. By default, HTTP checks will be - configured with a request timeout equal to 10 seconds. +## Types of checks - It is possible to configure a custom HTTP check timeout value by - specifying the `timeout` field in the check definition. The output of the - check is limited to roughly 4KB. Responses larger than this will be truncated. - HTTP checks also support TLS. By default, a valid TLS certificate is expected. - Certificate verification can be turned off by setting the `tls_skip_verify` - field to `true` in the check definition. When using TLS, the SNI will be set - automatically from the URL if it uses a hostname (as opposed to an IP address); - the value can be overridden by setting `tls_server_name`. +This section describes the available types of health checks you can use to +automatically monitor the health of a service instance or node. - Consul follows HTTP redirects by default. Set the `disable_redirects` field to - `true` to disable redirects. +-> **To manually mark a service unhealthy:** Use the maintenance mode + [CLI command](/commands/maint) or + [HTTP API endpoint](/api-docs/agent#enable-maintenance-mode) + to temporarily remove one or all service instances on a node + from service discovery DNS and HTTP API query results. -- `TCP + Interval` - These checks make a TCP connection attempt to the specified - IP/hostname and port, waiting `interval` amount of time between attempts - (e.g. 30 seconds). If no hostname - is specified, it defaults to "localhost". The status of the service depends on - whether the connection attempt is successful (ie - the port is currently - accepting connections). If the connection is accepted, the status is - `success`, otherwise the status is `critical`. In the case of a hostname that - resolves to both IPv4 and IPv6 addresses, an attempt will be made to both - addresses, and the first successful connection attempt will result in a - successful check. This type of check should be preferred over a script that - uses `netcat` or another external process to check a simple socket operation. - By default, TCP checks will be configured with a request timeout of 10 seconds. - It is possible to configure a custom TCP check timeout value by specifying the - `timeout` field in the check definition. +### Script check ((#script-interval)) -- `UDP + Interval` - These checks direct the client to periodically send UDP datagrams - to the specified IP/hostname and port. The duration specified in the `interval` field sets the amount of time - between attempts, such as `30s` to indicate 30 seconds. The check is logged as healthy if any response from the UDP server is received. Any other result sets the status to `critical`. - The default interval for, UDP checks is `10s`, but you can configure a custom UDP check timeout value by specifying the - `timeout` field in the check definition. If any timeout on read exists, the check is still considered healthy. +Script checks periodically invoke an external application that performs the health check, +exits with an appropriate exit code, and potentially generates some output. +The specified `interval` determines the time between check invocations. +The output of a script check is limited to 4KB. +Larger outputs are truncated. -- `Time to Live (TTL)` ((#ttl)) - These checks retain their last known state - for a given TTL. The state of the check must be updated periodically over the HTTP - interface. If an external system fails to update the status within a given TTL, - the check is set to the failed state. This mechanism, conceptually similar to a - dead man's switch, relies on the application to directly report its health. For - example, a healthy app can periodically `PUT` a status update to the HTTP endpoint; - if the app fails, the TTL will expire and the health check enters a critical state. - The endpoints used to update health information for a given check are: [pass](/api-docs/agent/check#ttl-check-pass), - [warn](/api-docs/agent/check#ttl-check-warn), [fail](/api-docs/agent/check#ttl-check-fail), - and [update](/api-docs/agent/check#ttl-check-update). TTL checks also persist their - last known status to disk. This allows the Consul agent to restore the last known - status of the check across restarts. Persisted check status is valid through the - end of the TTL from the time of the last check. +By default, script checks are configured with a timeout equal to 30 seconds. +To configure a custom script check timeout value, +specify the `timeout` field in the check definition. +After reaching the timeout on a Windows system, +Consul waits for any child processes spawned by the script to finish. +After reaching the timeout on other systems, +Consul attempts to force-kill the script and any child processes it spawned. -- `Docker + Interval` - These checks depend on invoking an external application which - is packaged within a Docker Container. The application is triggered within the running - container via the Docker Exec API. We expect that the Consul agent user has access - to either the Docker HTTP API or the unix socket. Consul uses `$DOCKER_HOST` to - determine the Docker API endpoint. The application is expected to run, perform a health - check of the service running inside the container, and exit with an appropriate exit code. - The check should be paired with an invocation interval. The shell on which the check - has to be performed is configurable which makes it possible to run containers which - have different shells on the same host. Check output for Docker is limited to - 4KB. Any output larger than this will be truncated. In Consul 0.9.0 and later, the agent - must be configured with [`enable_script_checks`](/docs/agent/config/cli-flags#_enable_script_checks) - set to `true` in order to enable Docker health checks. +Script checks are not enabled by default. +To enable a Consul agent to perform script checks, +use one of the following agent configuration options: -- `gRPC + Interval` - These checks are intended for applications that support the standard - [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - The state of the check will be updated by probing the configured endpoint, waiting `interval` - amount of time between probes (eg. 30 seconds). By default, gRPC checks will be configured - with a default timeout of 10 seconds. - It is possible to configure a custom timeout value by specifying the `timeout` field in - the check definition. gRPC checks will default to not using TLS, but TLS can be enabled by - setting `grpc_use_tls` in the check definition. If TLS is enabled, then by default, a valid - TLS certificate is expected. Certificate verification can be turned off by setting the - `tls_skip_verify` field to `true` in the check definition. - To check on a specific service instead of the whole gRPC server, add the service identifier after the `gRPC` check's endpoint in the following format `/:service_identifier`. +- [`enable_local_script_checks`](/docs/agent/config/cli-flags#_enable_local_script_checks): + Enable script checks defined in local config files. + Script checks registered using the HTTP API are not allowed. +- [`enable_script_checks`](/docs/agent/config/cli-flags#_enable_script_checks): + Enable script checks no matter how they are registered. -- `H2ping + Interval` - These checks test an endpoint that uses http2 - by connecting to the endpoint and sending a ping frame. TLS is assumed to be configured by default. - To disable TLS and use h2c, set `h2ping_use_tls` to `false`. If the ping is successful - within a specified timeout, then the check is updated as passing. - The timeout defaults to 10 seconds, but is configurable using the `timeout` field. If TLS is enabled a valid - certificate is required, unless `tls_skip_verify` is set to `true`. - The check will be run on the interval specified by the `interval` field. + ~> **Security Warning:** + Enabling non-local script checks in some configurations may introduce + a remote execution vulnerability known to be targeted by malware. + We strongly recommend `enable_local_script_checks` instead. + For more information, refer to + [this blog post](https://www.hashicorp.com/blog/protecting-consul-from-rce-risk-in-specific-configurations). -- `Alias` - These checks alias the health state of another registered - node or service. The state of the check will be updated asynchronously, but is - nearly instant. For aliased services on the same agent, the local state is monitored - and no additional network resources are consumed. For other services and nodes, - the check maintains a blocking query over the agent's connection with a current - server and allows stale requests. If there are any errors in watching the aliased - node or service, the check state will be critical. For the blocking query, the - check will use the ACL token set on the service or check definition or otherwise - will fall back to the default ACL token set with the agent (`acl_token`). - -## Check Definition - -A script check: +The following service definition file snippet is an example +of a script check definition: @@ -162,7 +90,6 @@ check = { interval = "10s" timeout = "1s" } - ``` ```json @@ -179,7 +106,47 @@ check = { -A HTTP check: +#### Check script conventions + +A check script's exit code is used to determine the health check status: + +- Exit code 0 - Check is passing +- Exit code 1 - Check is warning +- Any other code - Check is failing + +Any output of the script is captured and made available in the +`Output` field of checks included in HTTP API responses, +as in this example from the [local service health endpoint](/api-docs/agent/service#by-name-json). + +### HTTP check ((#http-interval)) + +HTTP checks periodically make an HTTP `GET` request to the specified URL, +waiting the specified `interval` amount of time between requests. +The status of the service depends on the HTTP response code: any `2xx` code is +considered passing, a `429 Too ManyRequests` is a warning, and anything else is +a failure. This type of check +should be preferred over a script that uses `curl` or another external process +to check a simple HTTP operation. By default, HTTP checks are `GET` requests +unless the `method` field specifies a different method. Additional request +headers can be set through the `header` field which is a map of lists of +strings, such as `{"x-foo": ["bar", "baz"]}`. + +By default, HTTP checks are configured with a request timeout equal to 10 seconds. +To configure a custom HTTP check timeout value, +specify the `timeout` field in the check definition. +The output of an HTTP check is limited to approximately 4KB. +Larger outputs are truncated. +HTTP checks also support TLS. By default, a valid TLS certificate is expected. +Certificate verification can be turned off by setting the `tls_skip_verify` +field to `true` in the check definition. When using TLS, the SNI is implicitly +determined from the URL if it uses a hostname instead of an IP address. +You can explicitly set the SNI value by setting `tls_server_name`. + +Consul follows HTTP redirects by default. +To disable redirects, set the `disable_redirects` field to `true`. + +The following service definition file snippet is an example +of an HTTP check definition: @@ -220,7 +187,23 @@ check = { -A TCP check: +### TCP check ((#tcp-interval)) + +TCP checks periodically make a TCP connection attempt to the specified IP/hostname and port, waiting `interval` amount of time between attempts. +If no hostname is specified, it defaults to "localhost". +The health check status is `success` if the target host accepts the connection attempt, +otherwise the status is `critical`. In the case of a hostname that +resolves to both IPv4 and IPv6 addresses, an attempt is made to both +addresses, and the first successful connection attempt results in a +successful check. This type of check should be preferred over a script that +uses `netcat` or another external process to check a simple socket operation. + +By default, TCP checks are configured with a request timeout equal to 10 seconds. +To configure a custom TCP check timeout value, +specify the `timeout` field in the check definition. + +The following service definition file snippet is an example +of a TCP check definition: @@ -232,7 +215,6 @@ check = { interval = "10s" timeout = "1s" } - ``` ```json @@ -249,7 +231,21 @@ check = { -A UDP check: +### UDP check ((#udp-interval)) + +UDP checks periodically direct the Consul agent to send UDP datagrams +to the specified IP/hostname and port, +waiting `interval` amount of time between attempts. +The check status is set to `success` if any response is received from the targeted UDP server. +Any other result sets the status to `critical`. + +By default, UDP checks are configured with a request timeout equal to 10 seconds. +To configure a custom UDP check timeout value, +specify the `timeout` field in the check definition. +If any timeout on read exists, the check is still considered healthy. + +The following service definition file snippet is an example +of a UDP check definition: @@ -261,7 +257,6 @@ check = { interval = "10s" timeout = "1s" } - ``` ```json @@ -278,7 +273,32 @@ check = { -A TTL check: +### Time to live (TTL) check ((#ttl)) + +TTL checks retain their last known state for the specified `ttl` duration. +If the `ttl` duration elapses before a new check update +is provided over the HTTP interface, +the check is set to `critical` state. + +This mechanism relies on the application to directly report its health. +For example, a healthy app can periodically `PUT` a status update to the HTTP endpoint. +Then, if the app is disrupted and unable to perform this update +before the TTL expires, the health check enters the `critical` state. +The endpoints used to update health information for a given check are: [pass](/api-docs/agent/check#ttl-check-pass), +[warn](/api-docs/agent/check#ttl-check-warn), [fail](/api-docs/agent/check#ttl-check-fail), +and [update](/api-docs/agent/check#ttl-check-update). TTL checks also persist their +last known status to disk. This persistence allows the Consul agent to restore the last known +status of the check across agent restarts. Persisted check status is valid through the +end of the TTL from the time of the last check. + +To manually mark a service unhealthy, +it is far more convenient to use the maintenance mode +[CLI command](/commands/maint) or +[HTTP API endpoint](/api-docs/agent#enable-maintenance-mode) +rather than a TTL health check with arbitrarily high `ttl`. + +The following service definition file snippet is an example +of a TTL check definition: @@ -304,7 +324,24 @@ check = { -A Docker check: +### Docker check ((#docker-interval)) + +These checks depend on periodically invoking an external application that +is packaged within a Docker Container. The application is triggered within the running +container through the Docker Exec API. We expect that the Consul agent user has access +to either the Docker HTTP API or the unix socket. Consul uses `$DOCKER_HOST` to +determine the Docker API endpoint. The application is expected to run, perform a health +check of the service running inside the container, and exit with an appropriate exit code. +The check should be paired with an invocation interval. The shell on which the check +has to be performed is configurable, making it possible to run containers which +have different shells on the same host. +The output of a Docker check is limited to 4KB. +Larger outputs are truncated. +The agent must be configured with [`enable_script_checks`](/docs/agent/config/cli-flags#_enable_script_checks) +set to `true` in order to enable Docker health checks. + +The following service definition file snippet is an example +of a Docker check definition: @@ -334,7 +371,26 @@ check = { -A gRPC check for the whole application: +### gRPC check ((##grpc-interval)) + +gRPC checks are intended for applications that support the standard +[gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md). +The state of the check will be updated by periodically probing the configured endpoint, +waiting `interval` amount of time between attempts. + +By default, gRPC checks are configured with a timeout equal to 10 seconds. +To configure a custom Docker check timeout value, +specify the `timeout` field in the check definition. + +gRPC checks default to not using TLS. +To enable TLS, set `grpc_use_tls` in the check definition. +If TLS is enabled, then by default, a valid TLS certificate is expected. +Certificate verification can be turned off by setting the +`tls_skip_verify` field to `true` in the check definition. +To check on a specific service instead of the whole gRPC server, add the service identifier after the `gRPC` check's endpoint in the following format `/:service_identifier`. + +The following service definition file snippet is an example +of a gRPC check for a whole application: @@ -362,7 +418,8 @@ check = { -A gRPC check for the specific `my_service` service: +The following service definition file snippet is an example +of a gRPC check for the specific `my_service` service @@ -390,7 +447,23 @@ check = { -A h2ping check: +### H2ping check ((#h2ping-interval)) + +H2ping checks test an endpoint that uses http2 by connecting to the endpoint +and sending a ping frame, waiting `interval` amount of time between attempts. +If the ping is successful within a specified timeout, +then the check status is set to `success`. + +By default, h2ping checks are configured with a request timeout equal to 10 seconds. +To configure a custom h2ping check timeout value, +specify the `timeout` field in the check definition. + +TLS is enabled by default. +To disable TLS and use h2c, set `h2ping_use_tls` to `false`. +If TLS is not disabled, a valid certificate is required unless `tls_skip_verify` is set to `true`. + +The following service definition file snippet is an example +of an h2ping check definition: @@ -418,7 +491,29 @@ check = { -An alias check for a local service: +### Alias check + +These checks alias the health state of another registered +node or service. The state of the check updates asynchronously, but is +nearly instant. For aliased services on the same agent, the local state is monitored +and no additional network resources are consumed. For other services and nodes, +the check maintains a blocking query over the agent's connection with a current +server and allows stale requests. If there are any errors in watching the aliased +node or service, the check state is set to `critical`. +For the blocking query, the check uses the ACL token set on the service or check definition. +If no ACL token is set in the service or check definition, +the blocking query uses the agent's default ACL token +([`acl.tokens.default`](/docs/agent/config/config-files#acl_tokens_default)). + +~> **Configuration info**: The alias check configuration expects the alias to be +registered on the same agent as the one you are aliasing. If the service is +not registered with the same agent, `"alias_node": ""` must also be +specified. When using `alias_node`, if no service is specified, the check will +alias the health of the node. If a service is specified, the check will alias +the specified service on this particular node. + +The following service definition file snippet is an example +of an alias check for a local service: @@ -440,72 +535,137 @@ check = { -~> Configuration info: The alias check configuration expects the alias to be -registered on the same agent as the one you are aliasing. If the service is -not registered with the same agent, `"alias_node": ""` must also be -specified. When using `alias_node`, if no service is specified, the check will -alias the health of the node. If a service is specified, the check will alias -the specified service on this particular node. +## Check definition -Each type of definition must include a `name` and may optionally provide an -`id` and `notes` field. The `id` must be unique per _agent_ otherwise only the -last defined check with that `id` will be registered. If the `id` is not set -and the check is embedded within a service definition a unique check id is -generated. Otherwise, `id` will be set to `name`. If names might conflict, -unique IDs should be provided. +This section covers some of the most common options for check definitions. +For a complete list of all check options, refer to the +[Register Check HTTP API endpoint documentation](/api-docs/agent/check#json-request-body-schema). -The `notes` field is opaque to Consul but can be used to provide a human-readable -description of the current state of the check. Similarly, an external process -updating a TTL check via the HTTP interface can set the `notes` value. +-> **Casing for check options:** + The correct casing for an option depends on whether the check is defined in + a service definition file or an HTTP API JSON request body. + For example, the option `deregister_critical_service_after` in a service + definition file is instead named `DeregisterCriticalServiceAfter` in an + HTTP API JSON request body. -Checks may also contain a `token` field to provide an ACL token. This token is -used for any interaction with the catalog for the check, including -[anti-entropy syncs](/docs/architecture/anti-entropy) and deregistration. -For Alias checks, this token is used if a remote blocking query is necessary -to watch the state of the aliased node or service. +#### General options -Script, TCP, UDP, HTTP, Docker, and gRPC checks must include an `interval` field. This -field is parsed by Go's `time` package, and has the following -[formatting specification](https://golang.org/pkg/time/#ParseDuration): +- `name` `(string: )` - Specifies the name of the check. -> A duration string is a possibly signed sequence of decimal numbers, each with -> optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". -> Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +- `id` `(string: "")` - Specifies a unique ID for this check on this node. + + If unspecified, Consul defines the check id by: + - If the check definition is embedded within a service definition file, + a unique check id is auto-generated. + - Otherwise, the `id` is set to the value of `name`. + If names might conflict, you must provide unique IDs to avoid + overwriting existing checks with the same id on this node. -In Consul 0.7 and later, checks that are associated with a service may also contain -an optional `deregister_critical_service_after` field, which is a timeout in the -same Go time format as `interval` and `ttl`. If a check is in the critical state -for more than this configured value, then its associated service (and all of its -associated checks) will automatically be deregistered. The minimum timeout is 1 -minute, and the process that reaps critical services runs every 30 seconds, so it -may take slightly longer than the configured timeout to trigger the deregistration. -This should generally be configured with a timeout that's much, much longer than -any expected recoverable outage for the given service. +- `interval` `(string: )` - Specifies + the frequency at which to run this check. + Required for all check types except TTL and alias checks. -To configure a check, either provide it as a `-config-file` option to the -agent or place it inside the `-config-dir` of the agent. The file must -end in a ".json" or ".hcl" extension to be loaded by Consul. Check definitions -can also be updated by sending a `SIGHUP` to the agent. Alternatively, the -check can be registered dynamically using the [HTTP API](/api). + The value is parsed by Go's `time` package, and has the following + [formatting specification](https://golang.org/pkg/time/#ParseDuration): -## Check Scripts + > A duration string is a possibly signed sequence of decimal numbers, each with + > optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". + > Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -A check script is generally free to do anything to determine the status -of the check. The only limitations placed are that the exit codes must obey -this convention: +- `service_id` `(string: )` - Specifies + the ID of a service instance to associate this check with. + That service instance must be on this node. + If not specified, this check is treated as a node-level check. + For more information, refer to the + [service-bound checks](#service-bound-checks) section. -- Exit code 0 - Check is passing -- Exit code 1 - Check is warning -- Any other code - Check is failing +- `status` `(string: "")` - Specifies the initial status of the health check as + "critical" (default), "warning", or "passing". For more details, refer to + the [initial health check status](#initial-health-check-status) section. + + -> **Health defaults to critical:** If health status it not initially specified, + it defaults to "critical" to protect against including a service + in discovery results before it is ready. -This is the only convention that Consul depends on. Any output of the script -will be captured and stored in the `output` field. +- `deregister_critical_service_after` `(string: "")` - If specified, + the associated service and all its checks are deregistered + after this check is in the critical state for more than the specified value. + The value has the same formatting specification as the [`interval`](#interval) field. -In Consul 0.9.0 and later, the agent must be configured with -[`enable_script_checks`](/docs/agent/config/cli-flags#_enable_script_checks) set to `true` -in order to enable script checks. + The minimum timeout is 1 minute, + and the process that reaps critical services runs every 30 seconds, + so it may take slightly longer than the configured timeout to trigger the deregistration. + This field should generally be configured with a timeout that's significantly longer than + any expected recoverable outage for the given service. -## Initial Health Check Status +- `notes` `(string: "")` - Provides a human-readable description of the check. + This field is opaque to Consul and can be used however is useful to the user. + For example, it could be used to describe the current state of the check. + +- `token` `(string: "")` - Specifies an ACL token used for any interaction + with the catalog for the check, including + [anti-entropy syncs](/docs/architecture/anti-entropy) and deregistration. + + For alias checks, this token is used if a remote blocking query is necessary to watch the state of the aliased node or service. + +#### Success/failures before passing/warning/critical + +To prevent flapping health checks and limit the load they cause on the cluster, +a health check may be configured to become passing/warning/critical only after a +specified number of consecutive checks return as passing/critical. +The status does not transition states until the configured threshold is reached. + +- `success_before_passing` - Number of consecutive successful results required + before check status transitions to passing. Defaults to `0`. Added in Consul 1.7.0. + +- `failures_before_warning` - Number of consecutive unsuccessful results required + before check status transitions to warning. Defaults to the same value as that of + `failures_before_critical` to maintain the expected behavior of not changing the + status of service checks to `warning` before `critical` unless configured to do so. + Values higher than `failures_before_critical` are invalid. Added in Consul 1.11.0. + +- `failures_before_critical` - Number of consecutive unsuccessful results required + before check status transitions to critical. Defaults to `0`. Added in Consul 1.7.0. + +This feature is available for all check types except TTL and alias checks. +By default, both passing and critical thresholds are set to 0 so the check +status always reflects the last check result. + + + +```hcl +checks = [ + { + name = "HTTP TCP on port 80" + tcp = "localhost:80" + interval = "10s" + timeout = "1s" + success_before_passing = 3 + failures_before_warning = 1 + failures_before_critical = 3 + } +] +``` + +```json +{ + "checks": [ + { + "name": "HTTP TCP on port 80", + "tcp": "localhost:80", + "interval": "10s", + "timeout": "1s", + "success_before_passing": 3, + "failures_before_warning": 1, + "failures_before_critical": 3 + } + ] +} +``` + + + +## Initial health check status By default, when checks are registered against a Consul agent, the state is set immediately to "critical". This is useful to prevent services from being @@ -576,13 +736,13 @@ In the above configuration, if the web-app health check begins failing, it will only affect the availability of the web-app service. All other services provided by the node will remain unchanged. -## Agent Certificates for TLS Checks +## Agent certificates for TLS checks The [enable_agent_tls_for_checks](/docs/agent/config/config-files#enable_agent_tls_for_checks) agent configuration option can be utilized to have HTTP or gRPC health checks to use the agent's credentials when configured for TLS. -## Multiple Check Definitions +## Multiple check definitions Multiple check definitions can be defined using the `checks` (plural) key in your configuration file. @@ -640,58 +800,3 @@ checks = [ ``` - -## Success/Failures before passing/warning/critical - -To prevent flapping health checks, and limit the load they cause on the cluster, -a health check may be configured to become passing/warning/critical only after a -specified number of consecutive checks return passing/critical. -The status will not transition states until the configured threshold is reached. - -- `success_before_passing` - Number of consecutive successful results required - before check status transitions to passing. Defaults to `0`. Added in Consul 1.7.0. -- `failures_before_warning` - Number of consecutive unsuccessful results required - before check status transitions to warning. Defaults to the same value as that of - `failures_before_critical` to maintain the expected behavior of not changing the - status of service checks to `warning` before `critical` unless configured to do so. - Values higher than `failures_before_critical` are invalid. Added in Consul 1.11.0. -- `failures_before_critical` - Number of consecutive unsuccessful results required - before check status transitions to critical. Defaults to `0`. Added in Consul 1.7.0. - -This feature is available for HTTP, TCP, gRPC, Docker & Monitor checks. -By default, both passing and critical thresholds will be set to 0 so the check -status will always reflect the last check result. - - - -```hcl -checks = [ - { - name = "HTTP TCP on port 80" - tcp = "localhost:80" - interval = "10s" - timeout = "1s" - success_before_passing = 3 - failures_before_warning = 1 - failures_before_critical = 3 - } -] -``` - -```json -{ - "checks": [ - { - "name": "HTTP TCP on port 80", - "tcp": "localhost:80", - "interval": "10s", - "timeout": "1s", - "success_before_passing": 3, - "failures_before_warning": 1, - "failures_before_critical": 3 - } - ] -} -``` - - From fead3c537b8f0a4325dec29661165d309f1cf3d0 Mon Sep 17 00:00:00 2001 From: Dao Thanh Tung Date: Fri, 26 Aug 2022 06:21:49 +0800 Subject: [PATCH 334/339] Fix Consul KV CLI 'GET' flags 'keys' and 'recurse' to be set together (#13493) allow flags -recurse and -keys to be run at the same time in consul kv get CLI --- .changelog/13493.txt | 3 ++ command/kv/get/kv_get.go | 36 +++++++++++-- command/kv/get/kv_get_test.go | 99 +++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 .changelog/13493.txt diff --git a/.changelog/13493.txt b/.changelog/13493.txt new file mode 100644 index 0000000000..9c3eec605d --- /dev/null +++ b/.changelog/13493.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: Fix Consul kv CLI 'GET' flags 'keys' and 'recurse' to be set together +``` diff --git a/command/kv/get/kv_get.go b/command/kv/get/kv_get.go index 099aedb9fc..aa93ef963b 100644 --- a/command/kv/get/kv_get.go +++ b/command/kv/get/kv_get.go @@ -99,6 +99,32 @@ func (c *cmd) Run(args []string) int { } switch { + case c.keys && c.recurse: + pairs, _, err := client.KV().List(key, &api.QueryOptions{ + AllowStale: c.http.Stale(), + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error querying Consul agent: %s", err)) + return 1 + } + + for i, pair := range pairs { + if c.detailed { + var b bytes.Buffer + if err := prettyKVPair(&b, pair, false, true); err != nil { + c.UI.Error(fmt.Sprintf("Error rendering KV key: %s", err)) + return 1 + } + c.UI.Info(b.String()) + + if i < len(pairs)-1 { + c.UI.Info("") + } + } else { + c.UI.Info(fmt.Sprintf("%s", pair.Key)) + } + } + return 0 case c.keys: keys, _, err := client.KV().Keys(key, c.separator, &api.QueryOptions{ AllowStale: c.http.Stale(), @@ -125,7 +151,7 @@ func (c *cmd) Run(args []string) int { for i, pair := range pairs { if c.detailed { var b bytes.Buffer - if err := prettyKVPair(&b, pair, c.base64encode); err != nil { + if err := prettyKVPair(&b, pair, c.base64encode, false); err != nil { c.UI.Error(fmt.Sprintf("Error rendering KV pair: %s", err)) return 1 } @@ -161,7 +187,7 @@ func (c *cmd) Run(args []string) int { if c.detailed { var b bytes.Buffer - if err := prettyKVPair(&b, pair, c.base64encode); err != nil { + if err := prettyKVPair(&b, pair, c.base64encode, false); err != nil { c.UI.Error(fmt.Sprintf("Error rendering KV pair: %s", err)) return 1 } @@ -187,7 +213,7 @@ func (c *cmd) Help() string { return c.help } -func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool) error { +func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool, keysOnly bool) error { tw := tabwriter.NewWriter(w, 0, 2, 6, ' ', 0) fmt.Fprintf(tw, "CreateIndex\t%d\n", pair.CreateIndex) fmt.Fprintf(tw, "Flags\t%d\n", pair.Flags) @@ -205,9 +231,9 @@ func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool) error { if pair.Namespace != "" { fmt.Fprintf(tw, "Namespace\t%s\n", pair.Namespace) } - if base64EncodeValue { + if !keysOnly && base64EncodeValue { fmt.Fprintf(tw, "Value\t%s", base64.StdEncoding.EncodeToString(pair.Value)) - } else { + } else if !keysOnly { fmt.Fprintf(tw, "Value\t%s", pair.Value) } return tw.Flush() diff --git a/command/kv/get/kv_get_test.go b/command/kv/get/kv_get_test.go index 3a7b12d8a7..5143391ef0 100644 --- a/command/kv/get/kv_get_test.go +++ b/command/kv/get/kv_get_test.go @@ -418,3 +418,102 @@ func TestKVGetCommand_DetailedBase64(t *testing.T) { t.Fatalf("bad %#v, value is not base64 encoded", output) } } + +func TestKVGetCommand_KeysRecurse(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := agent.NewTestAgent(t, ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + keys := map[string]string{ + "foo/": "", + "foo/a": "Hello World 2", + "foo1/a": "Hello World 1", + } + for k, v := range keys { + var pair *api.KVPair + switch v { + case "": + pair = &api.KVPair{Key: k, Value: nil} + default: + pair = &api.KVPair{Key: k, Value: []byte(v)} + } + if _, err := client.KV().Put(pair, nil); err != nil { + t.Fatalf("err: %#v", err) + } + } + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-recurse", + "-keys", + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + output := ui.OutputWriter.String() + for key, value := range keys { + if !strings.Contains(output, key) { + t.Fatalf("bad %#v missing %q", output, key) + } + if strings.Contains(output, key+":"+value) { + t.Fatalf("bad %#v expected no values for keys %q but received %q", output, key, value) + } + } +} +func TestKVGetCommand_DetailedKeysRecurse(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := agent.NewTestAgent(t, ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + keys := map[string]string{ + "foo/": "", + "foo/a": "Hello World 2", + "foo1/a": "Hello World 1", + } + for k, v := range keys { + var pair *api.KVPair + switch v { + case "": + pair = &api.KVPair{Key: k, Value: nil} + default: + pair = &api.KVPair{Key: k, Value: []byte(v)} + } + if _, err := client.KV().Put(pair, nil); err != nil { + t.Fatalf("err: %#v", err) + } + } + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-recurse", + "-keys", + "-detailed", + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + output := ui.OutputWriter.String() + for key, value := range keys { + if value != "" && strings.Contains(output, value) { + t.Fatalf("bad %#v expected no values for keys %q but received %q", output, key, value) + } + } +} From 30ff2e9a3596d79aaed0af1a322628996e5d109a Mon Sep 17 00:00:00 2001 From: alex <8968914+acpana@users.noreply.github.com> Date: Thu, 25 Aug 2022 16:32:59 -0700 Subject: [PATCH 335/339] peering: add peer health metric (#14004) Signed-off-by: acpana <8968914+acpana@users.noreply.github.com> --- agent/consul/leader_peering.go | 36 ++++-- agent/consul/leader_peering_test.go | 32 +++++ agent/consul/server.go | 1 + .../services/peerstream/server.go | 8 +- .../services/peerstream/stream_resources.go | 5 +- .../services/peerstream/stream_test.go | 52 +++++--- .../services/peerstream/stream_tracker.go | 62 +++++++++- .../peerstream/stream_tracker_test.go | 113 ++++++++++++++++-- 8 files changed, 262 insertions(+), 47 deletions(-) diff --git a/agent/consul/leader_peering.go b/agent/consul/leader_peering.go index d1823b026b..00128bcd87 100644 --- a/agent/consul/leader_peering.go +++ b/agent/consul/leader_peering.go @@ -31,11 +31,18 @@ import ( ) var leaderExportedServicesCountKey = []string{"consul", "peering", "exported_services"} +var leaderHealthyPeeringKey = []string{"consul", "peering", "healthy"} var LeaderPeeringMetrics = []prometheus.GaugeDefinition{ { Name: leaderExportedServicesCountKey, Help: "A gauge that tracks how many services are exported for the peering. " + - "The labels are \"peering\" and, for enterprise, \"partition\". " + + "The labels are \"peer_name\", \"peer_id\" and, for enterprise, \"partition\". " + + "We emit this metric every 9 seconds", + }, + { + Name: leaderHealthyPeeringKey, + Help: "A gauge that tracks how if a peering is healthy (1) or not (0). " + + "The labels are \"peer_name\", \"peer_id\" and, for enterprise, \"partition\". " + "We emit this metric every 9 seconds", }, } @@ -85,13 +92,6 @@ func (s *Server) emitPeeringMetricsOnce(logger hclog.Logger, metricsImpl *metric } for _, peer := range peers { - status, found := s.peerStreamServer.StreamStatus(peer.ID) - if !found { - logger.Trace("did not find status for", "peer_name", peer.Name) - continue - } - - esc := status.GetExportedServicesCount() part := peer.Partition labels := []metrics.Label{ {Name: "peer_name", Value: peer.Name}, @@ -101,7 +101,25 @@ func (s *Server) emitPeeringMetricsOnce(logger hclog.Logger, metricsImpl *metric labels = append(labels, metrics.Label{Name: "partition", Value: part}) } - metricsImpl.SetGaugeWithLabels(leaderExportedServicesCountKey, float32(esc), labels) + status, found := s.peerStreamServer.StreamStatus(peer.ID) + if found { + // exported services count metric + esc := status.GetExportedServicesCount() + metricsImpl.SetGaugeWithLabels(leaderExportedServicesCountKey, float32(esc), labels) + } + + // peering health metric + if status.NeverConnected { + metricsImpl.SetGaugeWithLabels(leaderHealthyPeeringKey, float32(math.NaN()), labels) + } else { + healthy := status.IsHealthy() + healthyInt := 0 + if healthy { + healthyInt = 1 + } + + metricsImpl.SetGaugeWithLabels(leaderHealthyPeeringKey, float32(healthyInt), labels) + } } return nil diff --git a/agent/consul/leader_peering_test.go b/agent/consul/leader_peering_test.go index 46a74b6ad3..d419303852 100644 --- a/agent/consul/leader_peering_test.go +++ b/agent/consul/leader_peering_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io/ioutil" + "math" "testing" "time" @@ -974,6 +975,7 @@ func TestLeader_PeeringMetrics_emitPeeringMetrics(t *testing.T) { var ( s2PeerID1 = generateUUID() s2PeerID2 = generateUUID() + s2PeerID3 = generateUUID() testContextTimeout = 60 * time.Second lastIdx = uint64(0) ) @@ -1063,6 +1065,24 @@ func TestLeader_PeeringMetrics_emitPeeringMetrics(t *testing.T) { // mimic tracking exported services mst2.TrackExportedService(structs.ServiceName{Name: "d-service"}) mst2.TrackExportedService(structs.ServiceName{Name: "e-service"}) + + // pretend that the hearbeat happened + mst2.TrackRecvHeartbeat() + } + + // Simulate a peering that never connects + { + p3 := &pbpeering.Peering{ + ID: s2PeerID3, + Name: "my-peer-s4", + PeerID: token.PeerID, // doesn't much matter what these values are + PeerCAPems: token.CA, + PeerServerName: token.ServerName, + PeerServerAddresses: token.ServerAddresses, + } + require.True(t, p3.ShouldDial()) + lastIdx++ + require.NoError(t, s2.fsm.State().PeeringWrite(lastIdx, &pbpeering.PeeringWriteRequest{Peering: p3})) } // set up a metrics sink @@ -1092,6 +1112,18 @@ func TestLeader_PeeringMetrics_emitPeeringMetrics(t *testing.T) { require.True(r, ok, fmt.Sprintf("did not find the key %q", keyMetric2)) require.Equal(r, float32(2), metric2.Value) // for d, e services + + keyHealthyMetric2 := fmt.Sprintf("us-west.consul.peering.healthy;peer_name=my-peer-s3;peer_id=%s", s2PeerID2) + healthyMetric2, ok := intv.Gauges[keyHealthyMetric2] + require.True(r, ok, fmt.Sprintf("did not find the key %q", keyHealthyMetric2)) + + require.Equal(r, float32(1), healthyMetric2.Value) + + keyHealthyMetric3 := fmt.Sprintf("us-west.consul.peering.healthy;peer_name=my-peer-s4;peer_id=%s", s2PeerID3) + healthyMetric3, ok := intv.Gauges[keyHealthyMetric3] + require.True(r, ok, fmt.Sprintf("did not find the key %q", keyHealthyMetric3)) + + require.True(r, math.IsNaN(float64(healthyMetric3.Value))) }) } diff --git a/agent/consul/server.go b/agent/consul/server.go index 1afa74c91d..8f2986c3eb 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -742,6 +742,7 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server) (*Ser return s.ForwardGRPC(s.grpcConnPool, info, fn) }, }) + s.peerStreamTracker.SetHeartbeatTimeout(s.peerStreamServer.Config.IncomingHeartbeatTimeout) s.peerStreamServer.Register(s.externalGRPCServer) // Initialize internal gRPC server. diff --git a/agent/grpc-external/services/peerstream/server.go b/agent/grpc-external/services/peerstream/server.go index 7254c60c7c..6568d7bf80 100644 --- a/agent/grpc-external/services/peerstream/server.go +++ b/agent/grpc-external/services/peerstream/server.go @@ -42,8 +42,8 @@ type Config struct { // outgoingHeartbeatInterval is how often we send a heartbeat. outgoingHeartbeatInterval time.Duration - // incomingHeartbeatTimeout is how long we'll wait between receiving heartbeats before we close the connection. - incomingHeartbeatTimeout time.Duration + // IncomingHeartbeatTimeout is how long we'll wait between receiving heartbeats before we close the connection. + IncomingHeartbeatTimeout time.Duration } //go:generate mockery --name ACLResolver --inpackage @@ -63,8 +63,8 @@ func NewServer(cfg Config) *Server { if cfg.outgoingHeartbeatInterval == 0 { cfg.outgoingHeartbeatInterval = defaultOutgoingHeartbeatInterval } - if cfg.incomingHeartbeatTimeout == 0 { - cfg.incomingHeartbeatTimeout = defaultIncomingHeartbeatTimeout + if cfg.IncomingHeartbeatTimeout == 0 { + cfg.IncomingHeartbeatTimeout = defaultIncomingHeartbeatTimeout } return &Server{ Config: cfg, diff --git a/agent/grpc-external/services/peerstream/stream_resources.go b/agent/grpc-external/services/peerstream/stream_resources.go index 657972b886..0e6b28f45a 100644 --- a/agent/grpc-external/services/peerstream/stream_resources.go +++ b/agent/grpc-external/services/peerstream/stream_resources.go @@ -406,7 +406,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { // incomingHeartbeatCtx will complete if incoming heartbeats time out. incomingHeartbeatCtx, incomingHeartbeatCtxCancel := - context.WithTimeout(context.Background(), s.incomingHeartbeatTimeout) + context.WithTimeout(context.Background(), s.IncomingHeartbeatTimeout) // NOTE: It's important that we wrap the call to cancel in a wrapper func because during the loop we're // re-assigning the value of incomingHeartbeatCtxCancel and we want the defer to run on the last assigned // value, not the current value. @@ -605,7 +605,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { // They just can't trace the execution properly for some reason (possibly golang/go#29587). //nolint:govet incomingHeartbeatCtx, incomingHeartbeatCtxCancel = - context.WithTimeout(context.Background(), s.incomingHeartbeatTimeout) + context.WithTimeout(context.Background(), s.IncomingHeartbeatTimeout) } case update := <-subCh: @@ -642,6 +642,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { if err := streamSend(replResp); err != nil { return fmt.Errorf("failed to push data for %q: %w", update.CorrelationID, err) } + status.TrackSendSuccess() } } } diff --git a/agent/grpc-external/services/peerstream/stream_test.go b/agent/grpc-external/services/peerstream/stream_test.go index 49ba7be046..be4a44ec87 100644 --- a/agent/grpc-external/services/peerstream/stream_test.go +++ b/agent/grpc-external/services/peerstream/stream_test.go @@ -572,7 +572,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { }) }) - var lastSendSuccess time.Time + var lastSendAck, lastSendSuccess time.Time testutil.RunStep(t, "ack tracked as success", func(t *testing.T) { ack := &pbpeerstream.ReplicationMessage{ @@ -587,19 +587,22 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { }, } - lastSendSuccess = it.FutureNow(1) + lastSendAck = time.Date(2000, time.January, 1, 0, 0, 2, 0, time.UTC) + lastSendSuccess = time.Date(2000, time.January, 1, 0, 0, 3, 0, time.UTC) err := client.Send(ack) require.NoError(t, err) expect := Status{ - Connected: true, - LastAck: lastSendSuccess, + Connected: true, + LastAck: lastSendAck, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, + LastSendSuccess: lastSendSuccess, } retry.Run(t, func(r *retry.R) { - status, ok := srv.StreamStatus(testPeerID) + rStatus, ok := srv.StreamStatus(testPeerID) require.True(r, ok) - require.Equal(r, expect, status) + require.Equal(r, expect, rStatus) }) }) @@ -621,23 +624,26 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { }, } - lastNack = it.FutureNow(1) + lastSendAck = time.Date(2000, time.January, 1, 0, 0, 4, 0, time.UTC) + lastNack = time.Date(2000, time.January, 1, 0, 0, 5, 0, time.UTC) err := client.Send(nack) require.NoError(t, err) lastNackMsg = "client peer was unable to apply resource: bad bad not good" expect := Status{ - Connected: true, - LastAck: lastSendSuccess, - LastNack: lastNack, - LastNackMessage: lastNackMsg, + Connected: true, + LastAck: lastSendAck, + LastNack: lastNack, + LastNackMessage: lastNackMsg, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, + LastSendSuccess: lastSendSuccess, } retry.Run(t, func(r *retry.R) { - status, ok := srv.StreamStatus(testPeerID) + rStatus, ok := srv.StreamStatus(testPeerID) require.True(r, ok) - require.Equal(r, expect, status) + require.Equal(r, expect, rStatus) }) }) @@ -694,13 +700,15 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, LastRecvResourceSuccess: lastRecvResourceSuccess, ImportedServices: map[string]struct{}{ api.String(): {}, }, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, + LastSendSuccess: lastSendSuccess, } retry.Run(t, func(r *retry.R) { @@ -753,7 +761,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, LastRecvResourceSuccess: lastRecvResourceSuccess, @@ -762,6 +770,8 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { ImportedServices: map[string]struct{}{ api.String(): {}, }, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, + LastSendSuccess: lastSendSuccess, } retry.Run(t, func(r *retry.R) { @@ -785,7 +795,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, LastRecvResourceSuccess: lastRecvResourceSuccess, @@ -795,6 +805,8 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { ImportedServices: map[string]struct{}{ api.String(): {}, }, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, + LastSendSuccess: lastSendSuccess, } retry.Run(t, func(r *retry.R) { @@ -816,7 +828,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: false, DisconnectErrorMessage: lastRecvErrorMsg, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, DisconnectTime: disconnectTime, @@ -827,6 +839,8 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { ImportedServices: map[string]struct{}{ api.String(): {}, }, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, + LastSendSuccess: lastSendSuccess, } retry.Run(t, func(r *retry.R) { @@ -1129,7 +1143,7 @@ func TestStreamResources_Server_DisconnectsOnHeartbeatTimeout(t *testing.T) { srv, store := newTestServer(t, func(c *Config) { c.Tracker.SetClock(it.Now) - c.incomingHeartbeatTimeout = 5 * time.Millisecond + c.IncomingHeartbeatTimeout = 5 * time.Millisecond }) p := writePeeringToBeDialed(t, store, 1, "my-peer") @@ -1236,7 +1250,7 @@ func TestStreamResources_Server_KeepsConnectionOpenWithHeartbeat(t *testing.T) { srv, store := newTestServer(t, func(c *Config) { c.Tracker.SetClock(it.Now) - c.incomingHeartbeatTimeout = incomingHeartbeatTimeout + c.IncomingHeartbeatTimeout = incomingHeartbeatTimeout }) p := writePeeringToBeDialed(t, store, 1, "my-peer") diff --git a/agent/grpc-external/services/peerstream/stream_tracker.go b/agent/grpc-external/services/peerstream/stream_tracker.go index f7a451595d..ffde98ba32 100644 --- a/agent/grpc-external/services/peerstream/stream_tracker.go +++ b/agent/grpc-external/services/peerstream/stream_tracker.go @@ -16,6 +16,8 @@ type Tracker struct { // timeNow is a shim for testing. timeNow func() time.Time + + heartbeatTimeout time.Duration } func NewTracker() *Tracker { @@ -33,6 +35,12 @@ func (t *Tracker) SetClock(clock func() time.Time) { } } +func (t *Tracker) SetHeartbeatTimeout(heartbeatTimeout time.Duration) { + t.mu.Lock() + defer t.mu.Unlock() + t.heartbeatTimeout = heartbeatTimeout +} + // Register a stream for a given peer but do not mark it as connected. func (t *Tracker) Register(id string) (*MutableStatus, error) { t.mu.Lock() @@ -44,7 +52,7 @@ func (t *Tracker) Register(id string) (*MutableStatus, error) { func (t *Tracker) registerLocked(id string, initAsConnected bool) (*MutableStatus, bool, error) { status, ok := t.streams[id] if !ok { - status = newMutableStatus(t.timeNow, initAsConnected) + status = newMutableStatus(t.timeNow, t.heartbeatTimeout, initAsConnected) t.streams[id] = status return status, true, nil } @@ -101,7 +109,9 @@ func (t *Tracker) StreamStatus(id string) (resp Status, found bool) { s, ok := t.streams[id] if !ok { - return Status{}, false + return Status{ + NeverConnected: true, + }, false } return s.GetStatus(), true } @@ -142,9 +152,14 @@ type MutableStatus struct { // Status contains information about the replication stream to a peer cluster. // TODO(peering): There's a lot of fields here... type Status struct { + heartbeatTimeout time.Duration + // Connected is true when there is an open stream for the peer. Connected bool + // NeverConnected is true for peerings that have never connected, false otherwise. + NeverConnected bool + // DisconnectErrorMessage tracks the error that caused the stream to disconnect non-gracefully. // If the stream is connected or it disconnected gracefully it will be empty. DisconnectErrorMessage string @@ -167,6 +182,9 @@ type Status struct { // LastSendErrorMessage tracks the last error message when sending into the stream. LastSendErrorMessage string + // LastSendSuccess tracks the time of the last success response sent into the stream. + LastSendSuccess time.Time + // LastRecvHeartbeat tracks when we last received a heartbeat from our peer. LastRecvHeartbeat time.Time @@ -196,10 +214,40 @@ func (s *Status) GetExportedServicesCount() uint64 { return uint64(len(s.ExportedServices)) } -func newMutableStatus(now func() time.Time, connected bool) *MutableStatus { +// IsHealthy is a convenience func that returns true/ false for a peering status. +// We define a peering as unhealthy if its status satisfies one of the following: +// - If heartbeat hasn't been received within the IncomingHeartbeatTimeout +// - If the last sent error is newer than last sent success +// - If the last received error is newer than last received success +// If none of these conditions apply, we call the peering healthy. +func (s *Status) IsHealthy() bool { + if time.Now().Sub(s.LastRecvHeartbeat) > s.heartbeatTimeout { + // 1. If heartbeat hasn't been received for a while - report unhealthy + return false + } + + if s.LastSendError.After(s.LastSendSuccess) { + // 2. If last sent error is newer than last sent success - report unhealthy + return false + } + + if s.LastRecvError.After(s.LastRecvResourceSuccess) { + // 3. If last recv error is newer than last recv success - report unhealthy + return false + } + + return true +} + +func newMutableStatus(now func() time.Time, heartbeatTimeout time.Duration, connected bool) *MutableStatus { + if heartbeatTimeout.Microseconds() == 0 { + heartbeatTimeout = defaultIncomingHeartbeatTimeout + } return &MutableStatus{ Status: Status{ - Connected: connected, + Connected: connected, + heartbeatTimeout: heartbeatTimeout, + NeverConnected: !connected, }, timeNow: now, doneCh: make(chan struct{}), @@ -223,6 +271,12 @@ func (s *MutableStatus) TrackSendError(error string) { s.mu.Unlock() } +func (s *MutableStatus) TrackSendSuccess() { + s.mu.Lock() + s.LastSendSuccess = s.timeNow().UTC() + s.mu.Unlock() +} + // TrackRecvResourceSuccess tracks receiving a replicated resource. func (s *MutableStatus) TrackRecvResourceSuccess() { s.mu.Lock() diff --git a/agent/grpc-external/services/peerstream/stream_tracker_test.go b/agent/grpc-external/services/peerstream/stream_tracker_test.go index f7a9df321d..8cdcbc79a2 100644 --- a/agent/grpc-external/services/peerstream/stream_tracker_test.go +++ b/agent/grpc-external/services/peerstream/stream_tracker_test.go @@ -10,6 +10,97 @@ import ( "github.com/hashicorp/consul/sdk/testutil" ) +const ( + aPeerID = "63b60245-c475-426b-b314-4588d210859d" +) + +func TestStatus_IsHealthy(t *testing.T) { + type testcase struct { + name string + dontConnect bool + modifierFunc func(status *MutableStatus) + expectedVal bool + heartbeatTimeout time.Duration + } + + tcs := []testcase{ + { + name: "never connected, unhealthy", + expectedVal: false, + dontConnect: true, + }, + { + name: "no heartbeat, unhealthy", + expectedVal: false, + }, + { + name: "heartbeat is not received, unhealthy", + expectedVal: false, + modifierFunc: func(status *MutableStatus) { + // set heartbeat + status.LastRecvHeartbeat = time.Now().Add(-1 * time.Second) + }, + heartbeatTimeout: 1 * time.Second, + }, + { + name: "send error before send success", + expectedVal: false, + modifierFunc: func(status *MutableStatus) { + // set heartbeat + status.LastRecvHeartbeat = time.Now() + + status.LastSendSuccess = time.Now() + status.LastSendError = time.Now() + }, + }, + { + name: "received error before received success", + expectedVal: false, + modifierFunc: func(status *MutableStatus) { + // set heartbeat + status.LastRecvHeartbeat = time.Now() + + status.LastRecvResourceSuccess = time.Now() + status.LastRecvError = time.Now() + }, + }, + { + name: "healthy", + expectedVal: true, + modifierFunc: func(status *MutableStatus) { + // set heartbeat + status.LastRecvHeartbeat = time.Now() + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tracker := NewTracker() + if tc.heartbeatTimeout.Microseconds() != 0 { + tracker.SetHeartbeatTimeout(tc.heartbeatTimeout) + } + + if !tc.dontConnect { + st, err := tracker.Connected(aPeerID) + require.NoError(t, err) + require.True(t, st.Connected) + + if tc.modifierFunc != nil { + tc.modifierFunc(st) + } + + require.Equal(t, tc.expectedVal, st.IsHealthy()) + + } else { + st, found := tracker.StreamStatus(aPeerID) + require.False(t, found) + require.Equal(t, tc.expectedVal, st.IsHealthy()) + } + }) + } +} + func TestTracker_EnsureConnectedDisconnected(t *testing.T) { tracker := NewTracker() peerID := "63b60245-c475-426b-b314-4588d210859d" @@ -29,7 +120,8 @@ func TestTracker_EnsureConnectedDisconnected(t *testing.T) { require.NoError(t, err) expect := Status{ - Connected: true, + Connected: true, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, } status, ok := tracker.StreamStatus(peerID) @@ -55,8 +147,9 @@ func TestTracker_EnsureConnectedDisconnected(t *testing.T) { lastSuccess = it.base.Add(time.Duration(sequence) * time.Second).UTC() expect := Status{ - Connected: true, - LastAck: lastSuccess, + Connected: true, + LastAck: lastSuccess, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, } require.Equal(t, expect, status) }) @@ -66,9 +159,10 @@ func TestTracker_EnsureConnectedDisconnected(t *testing.T) { sequence++ expect := Status{ - Connected: false, - DisconnectTime: it.base.Add(time.Duration(sequence) * time.Second).UTC(), - LastAck: lastSuccess, + Connected: false, + DisconnectTime: it.base.Add(time.Duration(sequence) * time.Second).UTC(), + LastAck: lastSuccess, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, } status, ok := tracker.StreamStatus(peerID) require.True(t, ok) @@ -80,8 +174,9 @@ func TestTracker_EnsureConnectedDisconnected(t *testing.T) { require.NoError(t, err) expect := Status{ - Connected: true, - LastAck: lastSuccess, + Connected: true, + LastAck: lastSuccess, + heartbeatTimeout: defaultIncomingHeartbeatTimeout, // DisconnectTime gets cleared on re-connect. } @@ -96,7 +191,7 @@ func TestTracker_EnsureConnectedDisconnected(t *testing.T) { status, ok := tracker.StreamStatus(peerID) require.False(t, ok) - require.Zero(t, status) + require.Equal(t, Status{NeverConnected: true}, status) }) } From 6ddcc046136d4e5a6a5f05fb59afdac0e070f7d3 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Fri, 26 Aug 2022 10:27:13 -0400 Subject: [PATCH 336/339] Replace ring buffer with async version (#14314) We need to watch for changes to peerings and update the server addresses which get served by the ring buffer. Also, if there is an active connection for a peer, we are getting up-to-date server addresses from the replication stream and can safely ignore the token's addresses which may be stale. --- agent/consul/leader_peering.go | 100 ++++++++++++++++---- agent/consul/leader_peering_test.go | 137 ++++++++++++++++++++++++++++ agent/consul/state/peering.go | 5 +- agent/rpc/peering/service.go | 16 +++- proto/pbpeering/peering.go | 4 +- 5 files changed, 233 insertions(+), 29 deletions(-) diff --git a/agent/consul/leader_peering.go b/agent/consul/leader_peering.go index 00128bcd87..556f1b5bfc 100644 --- a/agent/consul/leader_peering.go +++ b/agent/consul/leader_peering.go @@ -295,13 +295,6 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me return fmt.Errorf("failed to build TLS dial option from peering: %w", err) } - // Create a ring buffer to cycle through peer addresses in the retry loop below. - buffer := ring.New(len(peer.PeerServerAddresses)) - for _, addr := range peer.PeerServerAddresses { - buffer.Value = addr - buffer = buffer.Next() - } - secret, err := s.fsm.State().PeeringSecretsRead(ws, peer.ID) if err != nil { return fmt.Errorf("failed to read secret for peering: %w", err) @@ -312,27 +305,26 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me logger.Trace("establishing stream to peer") - retryCtx, cancel := context.WithCancel(ctx) - cancelFns[peer.ID] = cancel - streamStatus, err := s.peerStreamTracker.Register(peer.ID) if err != nil { return fmt.Errorf("failed to register stream: %v", err) } + streamCtx, cancel := context.WithCancel(ctx) + cancelFns[peer.ID] = cancel + + // Start a goroutine to watch for updates to peer server addresses. + // The latest valid server address can be received from nextServerAddr. + nextServerAddr := make(chan string) + go s.watchPeerServerAddrs(streamCtx, peer, nextServerAddr) + // Establish a stream-specific retry so that retrying stream/conn errors isn't dependent on state store changes. - go retryLoopBackoffPeering(retryCtx, logger, func() error { + go retryLoopBackoffPeering(streamCtx, logger, func() error { // Try a new address on each iteration by advancing the ring buffer on errors. - defer func() { - buffer = buffer.Next() - }() - addr, ok := buffer.Value.(string) - if !ok { - return fmt.Errorf("peer server address type %T is not a string", buffer.Value) - } + addr := <-nextServerAddr logger.Trace("dialing peer", "addr", addr) - conn, err := grpc.DialContext(retryCtx, addr, + conn, err := grpc.DialContext(streamCtx, addr, // TODO(peering): use a grpc.WithStatsHandler here?) tlsOption, // For keep alive parameters there is a larger comment in ClientConnPool.dial about that. @@ -349,7 +341,7 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me defer conn.Close() client := pbpeerstream.NewPeerStreamServiceClient(conn) - stream, err := client.StreamResources(retryCtx) + stream, err := client.StreamResources(streamCtx) if err != nil { return err } @@ -397,6 +389,74 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me return nil } +// watchPeerServerAddrs sends an up-to-date peer server address to nextServerAddr. +// It loads the server addresses into a ring buffer and cycles through them until: +// 1. streamCtx is cancelled (peer is deleted) +// 2. the peer is modified and the watchset fires. +// +// In case (2) we refetch the peering and rebuild the ring buffer. +func (s *Server) watchPeerServerAddrs(ctx context.Context, peer *pbpeering.Peering, nextServerAddr chan<- string) { + defer close(nextServerAddr) + + // we initialize the ring buffer with the peer passed to `establishStream` + // because the caller has pre-checked `peer.ShouldDial`, guaranteeing + // at least one server address. + // + // IMPORTANT: ringbuf must always be length > 0 or else `<-nextServerAddr` may block. + ringbuf := ring.New(len(peer.PeerServerAddresses)) + for _, addr := range peer.PeerServerAddresses { + ringbuf.Value = addr + ringbuf = ringbuf.Next() + } + innerWs := memdb.NewWatchSet() + _, _, err := s.fsm.State().PeeringReadByID(innerWs, peer.ID) + if err != nil { + s.logger.Warn("failed to watch for changes to peer; server addresses may become stale over time.", + "peer_id", peer.ID, + "error", err) + } + + fetchAddrs := func() error { + // reinstantiate innerWs to prevent it from growing indefinitely + innerWs = memdb.NewWatchSet() + _, peering, err := s.fsm.State().PeeringReadByID(innerWs, peer.ID) + if err != nil { + return fmt.Errorf("failed to fetch peer %q: %w", peer.ID, err) + } + if !peering.IsActive() { + return fmt.Errorf("peer %q is no longer active", peer.ID) + } + if len(peering.PeerServerAddresses) == 0 { + return fmt.Errorf("peer %q has no addresses to dial", peer.ID) + } + + ringbuf = ring.New(len(peering.PeerServerAddresses)) + for _, addr := range peering.PeerServerAddresses { + ringbuf.Value = addr + ringbuf = ringbuf.Next() + } + return nil + } + + for { + select { + case nextServerAddr <- ringbuf.Value.(string): + ringbuf = ringbuf.Next() + case err := <-innerWs.WatchCh(ctx): + if err != nil { + // context was cancelled + return + } + // watch fired so we refetch the peering and rebuild the ring buffer + if err := fetchAddrs(); err != nil { + s.logger.Warn("watchset for peer was fired but failed to update server addresses", + "peer_id", peer.ID, + "error", err) + } + } + } +} + func (s *Server) startPeeringDeferredDeletion(ctx context.Context) { s.leaderRoutineManager.Start(ctx, peeringDeletionRoutineName, s.runPeeringDeletions) } diff --git a/agent/consul/leader_peering_test.go b/agent/consul/leader_peering_test.go index d419303852..b8b5166d8f 100644 --- a/agent/consul/leader_peering_test.go +++ b/agent/consul/leader_peering_test.go @@ -18,6 +18,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" grpcstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" @@ -25,6 +26,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/sdk/freeport" + "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/types" @@ -1375,3 +1377,138 @@ func Test_isFailedPreconditionErr(t *testing.T) { werr := fmt.Errorf("wrapped: %w", err) assert.True(t, isFailedPreconditionErr(werr)) } + +func Test_Leader_PeeringSync_ServerAddressUpdates(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + // We want 1s retries for this test + orig := maxRetryBackoff + maxRetryBackoff = 1 + t.Cleanup(func() { maxRetryBackoff = orig }) + + _, acceptor := testServerWithConfig(t, func(c *Config) { + c.NodeName = "acceptor" + c.Datacenter = "dc1" + c.TLSConfig.Domain = "consul" + }) + testrpc.WaitForLeader(t, acceptor.RPC, "dc1") + + // Create a peering by generating a token + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + conn, err := grpc.DialContext(ctx, acceptor.config.RPCAddr.String(), + grpc.WithContextDialer(newServerDialer(acceptor.config.RPCAddr.String())), + grpc.WithInsecure(), + grpc.WithBlock()) + require.NoError(t, err) + defer conn.Close() + + acceptorClient := pbpeering.NewPeeringServiceClient(conn) + + req := pbpeering.GenerateTokenRequest{ + PeerName: "my-peer-dialer", + } + resp, err := acceptorClient.GenerateToken(ctx, &req) + require.NoError(t, err) + + // Bring up dialer and establish a peering with acceptor's token so that it attempts to dial. + _, dialer := testServerWithConfig(t, func(c *Config) { + c.NodeName = "dialer" + c.Datacenter = "dc2" + c.PrimaryDatacenter = "dc2" + }) + testrpc.WaitForLeader(t, dialer.RPC, "dc2") + + // Create a peering at dialer by establishing a peering with acceptor's token + ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + conn, err = grpc.DialContext(ctx, dialer.config.RPCAddr.String(), + grpc.WithContextDialer(newServerDialer(dialer.config.RPCAddr.String())), + grpc.WithInsecure(), + grpc.WithBlock()) + require.NoError(t, err) + defer conn.Close() + + dialerClient := pbpeering.NewPeeringServiceClient(conn) + + establishReq := pbpeering.EstablishRequest{ + PeerName: "my-peer-acceptor", + PeeringToken: resp.PeeringToken, + } + _, err = dialerClient.Establish(ctx, &establishReq) + require.NoError(t, err) + + p, err := dialerClient.PeeringRead(ctx, &pbpeering.PeeringReadRequest{Name: "my-peer-acceptor"}) + require.NoError(t, err) + + retry.Run(t, func(r *retry.R) { + status, found := dialer.peerStreamServer.StreamStatus(p.Peering.ID) + require.True(r, found) + require.True(r, status.Connected) + }) + + testutil.RunStep(t, "calling establish with active connection does not overwrite server addresses", func(t *testing.T) { + ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + // generate a new token from the acceptor + req := pbpeering.GenerateTokenRequest{ + PeerName: "my-peer-dialer", + } + resp, err := acceptorClient.GenerateToken(ctx, &req) + require.NoError(t, err) + + token, err := acceptor.peeringBackend.DecodeToken([]byte(resp.PeeringToken)) + require.NoError(t, err) + + // we will update the token with bad addresses to assert it doesn't clobber existing ones + token.ServerAddresses = []string{"1.2.3.4:1234"} + + badToken, err := acceptor.peeringBackend.EncodeToken(token) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + // Try establishing. + // This call will only succeed if the bad address was not used in the calls to exchange the peering secret. + establishReq := pbpeering.EstablishRequest{ + PeerName: "my-peer-acceptor", + PeeringToken: string(badToken), + } + _, err = dialerClient.Establish(ctx, &establishReq) + require.NoError(t, err) + + p, err := dialerClient.PeeringRead(ctx, &pbpeering.PeeringReadRequest{Name: "my-peer-acceptor"}) + require.NoError(t, err) + require.NotContains(t, p.Peering.PeerServerAddresses, "1.2.3.4:1234") + }) + + testutil.RunStep(t, "updated server addresses are picked up by the leader", func(t *testing.T) { + // force close the acceptor's gRPC server so the dialier retries with a new address. + acceptor.externalGRPCServer.Stop() + + clone := proto.Clone(p.Peering) + updated := clone.(*pbpeering.Peering) + // start with a bad address so we can assert for a specific error + updated.PeerServerAddresses = append([]string{ + "bad", + }, p.Peering.PeerServerAddresses...) + + // this write will wake up the watch on the leader to refetch server addresses + require.NoError(t, dialer.fsm.State().PeeringWrite(2000, &pbpeering.PeeringWriteRequest{Peering: updated})) + + retry.Run(t, func(r *retry.R) { + status, found := dialer.peerStreamServer.StreamStatus(p.Peering.ID) + require.True(r, found) + // We assert for this error to be set which would indicate that we iterated + // through a bad address. + require.Contains(r, status.LastSendErrorMessage, "transport: Error while dialing dial tcp: address bad: missing port in address") + require.False(r, status.Connected) + }) + }) +} diff --git a/agent/consul/state/peering.go b/agent/consul/state/peering.go index f56fbe0e15..287e822919 100644 --- a/agent/consul/state/peering.go +++ b/agent/consul/state/peering.go @@ -7,12 +7,13 @@ import ( "strings" "github.com/golang/protobuf/proto" + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib/maps" "github.com/hashicorp/consul/proto/pbpeering" - "github.com/hashicorp/go-memdb" ) const ( @@ -981,7 +982,7 @@ func peeringsForServiceTxn(tx ReadTxn, ws memdb.WatchSet, serviceName string, en if idx > maxIdx { maxIdx = idx } - if peering == nil || !peering.IsActive() { + if !peering.IsActive() { continue } peerings = append(peerings, peering) diff --git a/agent/rpc/peering/service.go b/agent/rpc/peering/service.go index ed9cd9e4fa..20bbafc1cf 100644 --- a/agent/rpc/peering/service.go +++ b/agent/rpc/peering/service.go @@ -8,7 +8,6 @@ import ( "time" "github.com/armon/go-metrics" - "github.com/hashicorp/consul/proto/pbpeerstream" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-multierror" @@ -27,6 +26,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/pbpeering" + "github.com/hashicorp/consul/proto/pbpeerstream" ) var ( @@ -379,6 +379,7 @@ func (s *Server) Establish( } var id string + serverAddrs := tok.ServerAddresses if existing == nil { id, err = lib.GenerateUUID(s.Backend.CheckPeeringUUID) if err != nil { @@ -386,6 +387,11 @@ func (s *Server) Establish( } } else { id = existing.ID + // If there is a connected stream, assume that the existing ServerAddresses + // are up to date and do not try to overwrite them with the token's addresses. + if status, ok := s.Tracker.StreamStatus(id); ok && status.Connected { + serverAddrs = existing.PeerServerAddresses + } } // validate that this peer name is not being used as an acceptor already @@ -397,7 +403,7 @@ func (s *Server) Establish( ID: id, Name: req.PeerName, PeerCAPems: tok.CA, - PeerServerAddresses: tok.ServerAddresses, + PeerServerAddresses: serverAddrs, PeerServerName: tok.ServerName, PeerID: tok.PeerID, Meta: req.Meta, @@ -418,9 +424,9 @@ func (s *Server) Establish( } var exchangeResp *pbpeerstream.ExchangeSecretResponse - // Loop through the token's addresses once, attempting to fetch the long-lived stream secret. + // Loop through the known server addresses once, attempting to fetch the long-lived stream secret. var dialErrors error - for _, addr := range peering.PeerServerAddresses { + for _, addr := range serverAddrs { exchangeResp, err = exchangeSecret(ctx, addr, tlsOption, &exchangeReq) if err != nil { dialErrors = multierror.Append(dialErrors, fmt.Errorf("failed to exchange peering secret with %q: %w", addr, err)) @@ -720,7 +726,7 @@ func (s *Server) PeeringDelete(ctx context.Context, req *pbpeering.PeeringDelete return nil, err } - if existing == nil || !existing.IsActive() { + if !existing.IsActive() { // Return early when the Peering doesn't exist or is already marked for deletion. // We don't return nil because the pb will fail to marshal. return &pbpeering.PeeringDeleteResponse{}, nil diff --git a/proto/pbpeering/peering.go b/proto/pbpeering/peering.go index d31328b589..74f5a52f08 100644 --- a/proto/pbpeering/peering.go +++ b/proto/pbpeering/peering.go @@ -143,10 +143,10 @@ func PeeringStateFromAPI(t api.PeeringState) PeeringState { } func (p *Peering) IsActive() bool { - if p != nil && p.State == PeeringState_TERMINATED { + if p == nil || p.State == PeeringState_TERMINATED { return false } - if p == nil || p.DeletedAt == nil { + if p.DeletedAt == nil { return true } From e2fe8b8d65638877ac4cc64c1957b69dac82ed7d Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Fri, 26 Aug 2022 11:14:02 -0400 Subject: [PATCH 337/339] Fix tests for enterprise --- agent/consul/state/catalog_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index ca2bded03b..e2310dbb7c 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -2104,10 +2104,13 @@ func TestStateStore_Services(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns1.EnterpriseMeta.Normalize() if err := s.EnsureService(2, "node1", ns1); err != nil { t.Fatalf("err: %s", err) } ns1Dogs := testRegisterService(t, s, 3, "node1", "dogs") + ns1Dogs.EnterpriseMeta.Normalize() + testRegisterNode(t, s, 4, "node2") ns2 := &structs.NodeService{ ID: "service3", @@ -2116,6 +2119,7 @@ func TestStateStore_Services(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns2.EnterpriseMeta.Normalize() if err := s.EnsureService(5, "node2", ns2); err != nil { t.Fatalf("err: %s", err) } @@ -2139,7 +2143,7 @@ func TestStateStore_Services(t *testing.T) { ns1.ToServiceNode("node1"), ns2.ToServiceNode("node2"), } - assertDeepEqual(t, services, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) + assertDeepEqual(t, expected, services, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) // Deleting a node with a service should fire the watch. if err := s.DeleteNode(6, "node1", nil, ""); err != nil { @@ -2178,6 +2182,7 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns1.EnterpriseMeta.Normalize() if err := s.EnsureService(2, "node0", ns1); err != nil { t.Fatalf("err: %s", err) } @@ -2188,6 +2193,7 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns2.EnterpriseMeta.Normalize() if err := s.EnsureService(3, "node1", ns2); err != nil { t.Fatalf("err: %s", err) } From eb0c5bb9a17ea2a6643eaea240fc3526bc8f129c Mon Sep 17 00:00:00 2001 From: smamindla57 <106655516+smamindla57@users.noreply.github.com> Date: Fri, 26 Aug 2022 21:43:46 +0530 Subject: [PATCH 338/339] Updated consul monitoring with Newrelic APM (#14360) * added newrelic consul quickstart link * adding HCP Consul Co-authored-by: David Yu --- website/content/docs/integrate/partnerships.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/content/docs/integrate/partnerships.mdx b/website/content/docs/integrate/partnerships.mdx index e7c5bb2228..057e3464cf 100644 --- a/website/content/docs/integrate/partnerships.mdx +++ b/website/content/docs/integrate/partnerships.mdx @@ -99,12 +99,13 @@ Here are links to resources, documentation, examples and best practices to guide - [Consul Telemetry Documentation](/docs/agent/telemetry) - [Monitoring Consul with Datadog APM](https://www.datadoghq.com/blog/consul-datadog/) - [Monitoring Consul with Dynatrace APM](https://www.dynatrace.com/news/blog/automatic-intelligent-observability-into-your-hashicorp-consul-service-mesh/) +- [Monitoring Consul with New Relic APM](https://newrelic.com/instant-observability/consul/b65825cc-faee-47b5-8d7c-6d60d6ab3c59) +- [Monitoring HCP Consul with New Relic APM](https://newrelic.com/instant-observability/hcp-consul/bc99ad15-7aba-450e-8236-6ea667d50cae) **Logging** - [Monitor Consul with Logz.io](https://www.hashicorp.com/integrations/logz-io/consul) - [Monitor Consul with Splunk SignalFx](https://www.hashicorp.com/integrations/splunksignalfx/consul) -- [Consul Datacenter Monitoring with New Relic](https://www.hashicorp.com/integrations/new-relic/consul) #### Platform: From b1ba0a89bc4c577fc65a21995ff7a25c199beaa0 Mon Sep 17 00:00:00 2001 From: David Yu Date: Fri, 26 Aug 2022 13:37:41 -0700 Subject: [PATCH 339/339] docs: Release notes for Consul 1.12, 1.13 and Consul K8s 0.47.0 (#14352) * consul 1.12, consul 1.13, and consul-k8s release notes Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com> --- website/content/docs/lambda/invocation.mdx | 2 +- .../consul-api-gateway/v0_1_x.mdx | 2 +- .../docs/release-notes/consul-k8s/v0_47_x.mdx | 48 +++++++++++++++++ .../docs/release-notes/consul/v1_10_x.mdx | 2 + .../docs/release-notes/consul/v1_11_x.mdx | 2 + .../docs/release-notes/consul/v1_12_x.mdx | 54 +++++++++++++++++++ .../docs/release-notes/consul/v1_13_x.mdx | 44 +++++++++++++++ website/data/docs-nav-data.json | 17 ++++++ 8 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 website/content/docs/release-notes/consul-k8s/v0_47_x.mdx create mode 100644 website/content/docs/release-notes/consul/v1_12_x.mdx create mode 100644 website/content/docs/release-notes/consul/v1_13_x.mdx diff --git a/website/content/docs/lambda/invocation.mdx b/website/content/docs/lambda/invocation.mdx index 991c8eb92d..4789c0adac 100644 --- a/website/content/docs/lambda/invocation.mdx +++ b/website/content/docs/lambda/invocation.mdx @@ -72,7 +72,7 @@ service mesh. } ``` 1. Issue the `consul services register` command to store the configuration: - ```shell-sesion + ```shell-session $ consul services register api-sidecar-proxy.hcl ``` 1. Call the upstream service to invoke the Lambda function. In the following example, the `api` service invokes the `authentication` service at `localhost:2345`: diff --git a/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx b/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx index 357fd7032d..b191e77fd2 100644 --- a/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx +++ b/website/content/docs/release-notes/consul-api-gateway/v0_1_x.mdx @@ -8,7 +8,7 @@ description: >- # Consul API Gateway 0.1.0 -## OVerview +## Overview This is the first general availability (GA) release of Consul API Gateway. It provides controlled access for network traffic from outside a Consul service diff --git a/website/content/docs/release-notes/consul-k8s/v0_47_x.mdx b/website/content/docs/release-notes/consul-k8s/v0_47_x.mdx new file mode 100644 index 0000000000..b040787c5c --- /dev/null +++ b/website/content/docs/release-notes/consul-k8s/v0_47_x.mdx @@ -0,0 +1,48 @@ +--- +layout: docs +page_title: 0.47.x +description: >- + Consul on Kubernetes release notes for version 0.47.x +--- + +# Consul on Kubernetes 0.47.0 + +## Release Highlights + +- **Cluster Peering (Beta)**: This release introduces support for Cluster Peering, which allows service connectivity between two independent clusters. Enabling peering will deploy the peering controllers and PeeringAcceptor and PeeringDialer CRDs. The new CRDs are used to establish a peering connection between two clusters. Refer to [Cluster Peering on Kubernetes](/docs/connect/cluster-peering/k8s) for full instructions on using Cluster Peering on Kubernetes. + +- **Envoy Proxy Debugging CLI Commands**: This release introduces new commands to quickly identify proxies and troubleshoot Envoy proxies for sidecars and gateways. + * Add `consul-k8s proxy list` command for displaying pods running Envoy managed by Consul. + * Add `consul-k8s proxy read podname` command for displaying Envoy configuration for a given pod + +- **Transparent Proxy Egress**: Adds support for destinations on the Service Defaults CRD when using transparent proxy for terminating gateways. + +## Supported Software + +- Consul 1.11.x, Consul 1.12.x and Consul 1.13.1+ +- Kubernetes 1.19+ + - Kubernetes 1.24 is not supported at this time. +- Kubectl 1.21+ +- Envoy proxy support is determined by the Consul version deployed. Refer to + [Envoy Integration](/docs/connect/proxies/envoy) for details. + +## Upgrading + +For detailed information on upgrading, please refer to the [Upgrades page](/docs/k8s/upgrade) + +## Known Issues + +The following issues are know to exist in the v0.47.0 and v0.47.1 releases + +- Kubernetes 1.24 is not supported because secret-based tokens are no longer autocreated by default for service accounts. Refer to GitHub issue + [[GH-1145](https://github.com/hashicorp/consul-k8s/issues/1145)] for + details. + +## Changelogs + +The changelogs for this major release version and any maintenance versions are listed below. + +~> **Note:** The following link takes you to the changelogs on the GitHub website. + +- [0.47.0](https://github.com/hashicorp/consul-k8s/releases/tag/v0.47.0) +- [0.47.1](https://github.com/hashicorp/consul-k8s/releases/tag/v0.47.1) diff --git a/website/content/docs/release-notes/consul/v1_10_x.mdx b/website/content/docs/release-notes/consul/v1_10_x.mdx index e4531dec08..e55b2ce32c 100644 --- a/website/content/docs/release-notes/consul/v1_10_x.mdx +++ b/website/content/docs/release-notes/consul/v1_10_x.mdx @@ -24,6 +24,8 @@ description: >- - Drops support for Envoy version 1.13.x. - (Enterprise Only) Consul Enterprise has removed support for temporary licensing. All server agents must have a valid license at startup and client agents must have a license at startup or be able to retrieve one from the servers. +## Upgrading + For more detailed information, please refer to the [upgrade details page](/docs/upgrading/upgrade-specific#consul-1-10-0) and the changelogs. ## Changelogs diff --git a/website/content/docs/release-notes/consul/v1_11_x.mdx b/website/content/docs/release-notes/consul/v1_11_x.mdx index eeb468003d..d26cd6a804 100644 --- a/website/content/docs/release-notes/consul/v1_11_x.mdx +++ b/website/content/docs/release-notes/consul/v1_11_x.mdx @@ -27,6 +27,8 @@ description: >- - Drops support for Envoy versions 1.15.x and 1.16.x +## Upgrading + For more detailed information, please refer to the [upgrade details page](/docs/upgrading/upgrade-specific#consul-1-11-0) and the changelogs. ## Changelogs diff --git a/website/content/docs/release-notes/consul/v1_12_x.mdx b/website/content/docs/release-notes/consul/v1_12_x.mdx new file mode 100644 index 0000000000..842dfb31c8 --- /dev/null +++ b/website/content/docs/release-notes/consul/v1_12_x.mdx @@ -0,0 +1,54 @@ +--- +layout: docs +page_title: 1.12.x +description: >- + Consul release notes for version 1.12.x +--- + +# Consul 1.12.0 + +## Release Highlights + +- **AWS IAM Auth Method**: Consul now provides an AWS IAM auth method that allows AWS IAM roles and users to authenticate with Consul to obtain ACL tokens. Refer to [AWS IAM Auth Method](/docs/security/acl/auth-methods/aws-iam) for detailed configuration information. + +- **Per listener TLS Config**: It is now possible to configure TLS differently for each of Consul's listeners, such as HTTPS, gRPC, and the internal multiplexed RPC listener, using the `tls` stanza. Refer to [TLS Configuration Reference](/docs/agent/config/config-files#tls-configuration-reference) for more details. + +- **AWS Lambda**: Adds the ability to invoke AWS Lambdas through terminating gateways, which allows for cross-datacenter communication, transparent proxy, and intentions with Consul Service Mesh. Refer to [AWS Lambda](/docs]/lambda) and [Invoke Lambda Functions](/docs/lambda/invocation) for more details. + +- **Mesh-wide TLS min/max versions and cipher suites:** Using the [Mesh](/docs/connect/config-entries/mesh#tls) Config Entry or CRD, it is now possible to set TLS min/max versions and cipher suites for both inbound and outbound mTLS connections. + +- **Expanded details for ACL Permission Denied errors**: Details are now provided when a permission denied errors surface for RPC calls. Details include the accessor ID of the ACL token, the missing permission, and any namespace or partition that the error occurred on. + +- **ACL token read**: The `consul acl token read -rules` command now includes an `-expanded` option to display detailed info about any policies and rules affecting the token. Refer to [Consul ACL Token read](/commands/acl/token/read) for more details. + +- **Automatically reload agent config when watching agent config file changes**: When using the `auto-reload-config` CLI flag or `auto_reload_config` agent config option, Consul now automatically reloads the [reloadable configuration options](/docs/agent/config#reloadable-configuration) when configuration files change. Refer to [auto_reload_config](/docs/agent/config/cli-flags#_auto_reload_config) for more details. + + +## What's Changed + +- Removes support for Envoy 1.17.x and Envoy 1.18.x, and adds support for Envoy 1.21.x and Envoy 1.22.x. Refer to the [Envoy Compatibility matrix](/docs/connect/proxies/envoy) for more details. + +- The `disable_compat_1.9` option now defaults to true. Metrics formatted in the style of version 1.9, such as `consul.http...`, can still be enabled by setting disable_compat_1.9 = false. However, these metrics will be removed in 1.13. + +- The `agent_master` ACL token has been renamed to `agent_recovery` ACL token. In addition, the `consul acl set-agent-token master` command has been replaced with `consul acl set-agent-token recovery`. Refer to [ACL Agent Recovery Token](/docs/security/acl/acl-tokens#acl-agent-recovery-token) and [Consul ACL Set Agent Token](/commands/acl/set-agent-token) for more information. + +- If TLS min versions and max versions are not specified, the TLS min/max versions default to the following values. For details on how to configure TLS min and max, refer to the [Mesh TLS config entry](/docs/connect/config-entries/mesh#tls) or CRD documentation. + - Incoming connections: TLS 1.2 for min0 version, TLS 1.3 for max version + - Outgoing connections: TLS 1.2 for both TLS min and TLS max versions. + +## Upgrading + +For more detailed information, please refer to the [upgrade details page](/docs/upgrading/upgrade-specific#consul-1-12-0) and the changelogs. + +## Changelogs + +The changelogs for this major release version and any maintenance versions are listed below. + +-> **Note**: These links take you to the changelogs on the GitHub website. + +- [1.12.0](https://github.com/hashicorp/consul/releases/tag/v1.12.0) +- [1.12.1](https://github.com/hashicorp/consul/releases/tag/v1.12.1) +- [1.12.2](https://github.com/hashicorp/consul/releases/tag/v1.12.2) +- [1.12.3](https://github.com/hashicorp/consul/releases/tag/v1.12.3) +- [1.12.4](https://github.com/hashicorp/consul/releases/tag/v1.12.4) + diff --git a/website/content/docs/release-notes/consul/v1_13_x.mdx b/website/content/docs/release-notes/consul/v1_13_x.mdx new file mode 100644 index 0000000000..23b694a913 --- /dev/null +++ b/website/content/docs/release-notes/consul/v1_13_x.mdx @@ -0,0 +1,44 @@ +--- +layout: docs +page_title: 1.13.x +description: >- + Consul release notes for version 1.13.x +--- + +# Consul 1.13.0 + +## Release Highlights + +- **Cluster Peering (Beta)**: This version adds a new model to federate Consul clusters for both service mesh and traditional service discovery. Cluster peering allows for service interconnectivity with looser coupling than the existing WAN federation. For more information, refer to the [cluster peering](/docs/connect/cluster-peering) documentation. + +- **Transparent proxying through terminating gateways**: This version adds egress traffic control to destinations outside of Consul's catalog, such as APIs on the public internet. Transparent proxies can dial [destinations defined in service-defaults](/docs/connect/config-entries/service-defaults#destination) and have the traffic routed through terminating gateways. For more information, refer to the [terminating gateway](/docs/connect/gateways/terminating-gateway#terminating-gateway-configuration) documentation. + +- **Enables TLS on the Envoy Prometheus endpoint**: The Envoy prometheus endpoint can be enabled when `envoy_prometheus_bind_addr` is set and then secured over TLS using new CLI flags for the `consul connect envoy` command. These commands are: `-prometheus-ca-file`, `-prometheus-ca-path`, `-prometheus-cert-file` and `-prometheus-key-file`. The CA, cert, and key can be provided to Envoy by a Kubernetes mounted volume so that Envoy can watch the files and dynamically reload the certs when the volume is updated. + +- **UDP Health Checks**: Adds the ability to register service discovery health checks that periodically send UDP datagrams to the specified IP/hostname and port. Refer to [UDP checks](/docs/discovery/checks#udp-interval). + +## What's Changed + +- Removes support for Envoy 1.19.x and adds suport for Envoy 1.23. Refer to the [Envoy Compatibility matrix](/docs/connect/proxies/envoy) for more details. + +- The [`disable_compat_19`](/docs/agent/options#telemetry-disable_compat_1.9) telemetry configuration option is now removed. In Consul versions 1.10.x through 1.11.x, the config defaulted to `false`. In version 1.12.x it defaulted to `true`. Before upgrading you should remove this flag from your config if the flag is being used. + +## Upgrading + +For more detailed information, please refer to the [upgrade details page](/docs/upgrading/upgrade-specific#consul-1-13-0) and the changelogs. + +## Known Issues +The following issues are know to exist in the 1.13.0 release: + +- Consul 1.13.1 fixes a compatibility issue when restoring snapshots from pre-1.13.0 versions of Consul. Refer to GitHub issue [[GH-14149](https://github.com/hashicorp/consul/issues/14149)] for more details. +- Consul 1.13.0 and Consul 1.13.1 default to requiring TLS for gRPC communication with Envoy proxies when auto-encrypt and auto-config are enabled. In environments where Envoy proxies are not already configured to use TLS for gRPC, upgrading Consul 1.13 will cause Envoy proxies to disconnect from the control plane (Consul agents). A future patch release will default to disabling TLS by default for GRPC communication with Envoy proxies when using Service Mesh and auto-config or auto-encrypt. Refer to GitHub issue [GH-14253](https://github.com/hashicorp/consul/issues/14253) and [Service Mesh deployments using auto-config and auto-enrypt](https://www.consul.io/docs/upgrading/upgrade-specific#service-mesh-deployments-using-auto-encrypt-or-auto-config) for more details. + + +## Changelogs + +The changelogs for this major release version and any maintenance versions are listed below. + +-> **Note**: These links take you to the changelogs on the GitHub website. + +- [1.13.0](https://github.com/hashicorp/consul/releases/tag/v1.13.0) +- [1.13.1](https://github.com/hashicorp/consul/releases/tag/v1.13.1) diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 7f64ed3b45..49b1d91100 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -1248,6 +1248,14 @@ { "title": "Consul", "routes": [ + { + "title": "v1.13.x", + "path": "release-notes/consul/v1_13_x" + }, + { + "title": "v1.12.x", + "path": "release-notes/consul/v1_12_x" + }, { "title": "v1.11.x", "path": "release-notes/consul/v1_11_x" @@ -1262,6 +1270,15 @@ } ] }, + { + "title": "Consul K8s", + "routes": [ + { + "title": "v0.47.x", + "path": "release-notes/consul-k8s/v0_47_x" + } + ] + }, { "title": "Consul API Gateway", "routes": [