Skip to content

Commit

Permalink
backport of commit 9ec2b38
Browse files Browse the repository at this point in the history
  • Loading branch information
ramramhariram authored Jun 12, 2023
1 parent baaf6d8 commit 751e11b
Show file tree
Hide file tree
Showing 43 changed files with 2,970 additions and 231 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

name: Nightly Test 1.16.x
name: Nightly Test 1.12.x
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch: {}

env:
EMBER_PARTITION_TOTAL: 4 # Has to be changed in tandem with the matrix.partition
BRANCH: "release/1.16.x"
BRANCH_NAME: "release-1.16.x" # Used for naming artifacts
BRANCH: "release/1.12.x"
BRANCH_NAME: "release-1.12.x" # Used for naming artifacts

jobs:
frontend-test-workspace-node:
Expand Down
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
## 1.16.0-rc1 (June 12, 2023)

BREAKING CHANGES:

* api: The `/v1/health/connect/` and `/v1/health/ingress/` endpoints now immediately return 403 "Permission Denied" errors whenever a token with insufficient `service:read` permissions is provided. Prior to this change, the endpoints returned a success code with an empty result list when a token with insufficient permissions was provided. [[GH-17424](https://github.com/hashicorp/consul/issues/17424)]
* peering: Removed deprecated backward-compatibility behavior.
Upstream overrides in service-defaults will now only apply to peer upstreams when the `peer` field is provided.
Visit the 1.16.x [upgrade instructions](https://developer.hashicorp.com/consul/docs/upgrading/upgrade-specific) for more information. [[GH-16957](https://github.com/hashicorp/consul/issues/16957)]

SECURITY:

* audit-logging: **(Enterprise only)** limit `v1/operator/audit-hash` endpoint to ACL token with `operator:read` privileges.

FEATURES:

* api: (Enterprise only) Add `POST /v1/operator/audit-hash` endpoint to calculate the hash of the data used by the audit log hash function and salt.
* cli: (Enterprise only) Add a new `consul operator audit hash` command to retrieve and compare the hash of the data used by the audit log hash function and salt.
* cli: Adds new command - `consul services export` - for exporting a service to a peer or partition [[GH-15654](https://github.com/hashicorp/consul/issues/15654)]
* connect: **(Consul Enterprise only)** Implement order-by-locality failover.
* mesh: Add new permissive mTLS mode that allows sidecar proxies to forward incoming traffic unmodified to the application. This adds `AllowEnablingPermissiveMutualTLS` setting to the mesh config entry and the `MutualTLSMode` setting to proxy-defaults and service-defaults. [[GH-17035](https://github.com/hashicorp/consul/issues/17035)]
* mesh: Support configuring JWT authentication in Envoy. [[GH-17452](https://github.com/hashicorp/consul/issues/17452)]
* server: **(Enterprise Only)** added server side RPC requests IP based read/write rate-limiter. [[GH-4633](https://github.com/hashicorp/consul/issues/4633)]
* server: **(Enterprise Only)** allow automatic license utilization reporting. [[GH-5102](https://github.com/hashicorp/consul/issues/5102)]
* server: added server side RPC requests global read/write rate-limiter. [[GH-16292](https://github.com/hashicorp/consul/issues/16292)]
* xds: Add `property-override` built-in Envoy extension that directly patches Envoy resources. [[GH-17487](https://github.com/hashicorp/consul/issues/17487)]
* xds: Add a built-in Envoy extension that inserts External Authorization (ext_authz) network and HTTP filters. [[GH-17495](https://github.com/hashicorp/consul/issues/17495)]
* xds: Add a built-in Envoy extension that inserts Wasm HTTP filters. [[GH-16877](https://github.com/hashicorp/consul/issues/16877)]
* xds: Add a built-in Envoy extension that inserts Wasm network filters. [[GH-17505](https://github.com/hashicorp/consul/issues/17505)]

IMPROVEMENTS:

* * api: Support filtering for config entries. [[GH-17183](https://github.com/hashicorp/consul/issues/17183)]
* * cli: Add `-filter` option to `consul config list` for filtering config entries. [[GH-17183](https://github.com/hashicorp/consul/issues/17183)]
* api: Enable setting query options on agent force-leave endpoint. [[GH-15987](https://github.com/hashicorp/consul/issues/15987)]
* audit-logging: (Enterprise only) enable error response and request body logging [[GH-5669](https://github.com/hashicorp/consul/issues/5669)]
* audit-logging: **(Enterprise only)** enable error response and request body logging
* ca: automatically set up Vault's auto-tidy setting for tidy_expired_issuers when using Vault as a CA provider. [[GH-17138](https://github.com/hashicorp/consul/issues/17138)]
* ca: support Vault agent auto-auth config for Vault CA provider using AliCloud authentication. [[GH-16224](https://github.com/hashicorp/consul/issues/16224)]
* ca: support Vault agent auto-auth config for Vault CA provider using AppRole authentication. [[GH-16259](https://github.com/hashicorp/consul/issues/16259)]
* ca: support Vault agent auto-auth config for Vault CA provider using Azure MSI authentication. [[GH-16298](https://github.com/hashicorp/consul/issues/16298)]
* ca: support Vault agent auto-auth config for Vault CA provider using JWT authentication. [[GH-16266](https://github.com/hashicorp/consul/issues/16266)]
* ca: support Vault agent auto-auth config for Vault CA provider using Kubernetes authentication. [[GH-16262](https://github.com/hashicorp/consul/issues/16262)]
* command: Adds ACL enabled to status output on agent startup. [[GH-17086](https://github.com/hashicorp/consul/issues/17086)]
* command: Allow creating ACL Token TTL with greater than 24 hours with the -expires-ttl flag. [[GH-17066](https://github.com/hashicorp/consul/issues/17066)]
* connect: **(Enterprise Only)** Add support for specifying "Partition" and "Namespace" in Prepared Queries failover rules.
* connect: update supported envoy versions to 1.23.10, 1.24.8, 1.25.7, 1.26.2 [[GH-17546](https://github.com/hashicorp/consul/issues/17546)]
* connect: update supported envoy versions to 1.23.8, 1.24.6, 1.25.4, 1.26.0 [[GH-5200](https://github.com/hashicorp/consul/issues/5200)]
* fix metric names in /docs/agent/telemetry [[GH-17577](https://github.com/hashicorp/consul/issues/17577)]
* gateway: Change status condition reason for invalid certificate on a listener from "Accepted" to "ResolvedRefs". [[GH-17115](https://github.com/hashicorp/consul/issues/17115)]
* http: accept query parameters `datacenter`, `ap` (enterprise-only), and `namespace` (enterprise-only). Both short-hand and long-hand forms of these query params are now supported via the HTTP API (dc/datacenter, ap/partition, ns/namespace). [[GH-17525](https://github.com/hashicorp/consul/issues/17525)]
* systemd: set service type to notify. [[GH-16845](https://github.com/hashicorp/consul/issues/16845)]
* ui: Update alerts to Hds::Alert component [[GH-16412](https://github.com/hashicorp/consul/issues/16412)]
* ui: Update to use Hds::Toast component to show notifications [[GH-16519](https://github.com/hashicorp/consul/issues/16519)]
* ui: update from <button> and <a> to design-system-components button <Hds::Button> [[GH-16251](https://github.com/hashicorp/consul/issues/16251)]
* ui: update typography to styles from hds [[GH-16577](https://github.com/hashicorp/consul/issues/16577)]

BUG FIXES:

* Fix a race condition where an event is published before the data associated is commited to memdb. [[GH-16871](https://github.com/hashicorp/consul/issues/16871)]
* gateways: **(Enterprise only)** Fixed a bug in API gateways where gateway configuration objects in non-default partitions did not reconcile properly. [[GH-17581](https://github.com/hashicorp/consul/issues/17581)]
* gateways: Fixed a bug in API gateways where binding a route that only targets a service imported from a peer results
in the programmed gateway having no routes. [[GH-17609](https://github.com/hashicorp/consul/issues/17609)]
* gateways: Fixed a bug where API gateways were not being taken into account in determining xDS rate limits. [[GH-17631](https://github.com/hashicorp/consul/issues/17631)]
* peering: Fixes a bug where the importing partition was not added to peered failover targets, which causes issues when the importing partition is a non-default partition. [[GH-16673](https://github.com/hashicorp/consul/issues/16673)]
* ui: fixes ui tests run on CI [[GH-16428](https://github.com/hashicorp/consul/issues/16428)]
* xds: Fixed a bug where modifying ACLs on a token being actively used for an xDS connection caused all xDS updates to fail. [[GH-17566](https://github.com/hashicorp/consul/issues/17566)]

## 1.15.3 (June 1, 2023)

BREAKING CHANGES:
Expand Down
10 changes: 6 additions & 4 deletions agent/agent_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import (
"strings"
"time"

"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
"github.com/mitchellh/hashstructure"

"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/version"

"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/serf/coordinate"
"github.com/hashicorp/serf/serf"
"github.com/mitchellh/hashstructure"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"

Expand All @@ -27,13 +31,11 @@ import (
"github.com/hashicorp/consul/agent/structs"
token_store "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/logging/monitor"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/version"
)

type Self struct {
Expand Down
6 changes: 4 additions & 2 deletions agent/agent_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import (
"time"

"github.com/armon/go-metrics"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/version"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/serf/serf"
Expand All @@ -40,14 +44,12 @@ import (
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
tokenStore "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/version"
)

func createACLTokenWithAgentReadPolicy(t *testing.T, srv *HTTPHandlers) string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ func (r *ratelimit) fromArguments(args map[string]interface{}) error {
if err := mapstructure.Decode(args, r); err != nil {
return fmt.Errorf("error decoding extension arguments: %v", err)
}
if r.ProxyType == "" {
r.ProxyType = string(api.ServiceKindConnectProxy)
}
return r.validate()
}

Expand Down Expand Up @@ -188,7 +191,7 @@ func (r ratelimit) PatchFilter(p extensioncommon.FilterPayload) (*envoy_listener
}

func validateProxyType(t string) error {
if t != "connect-proxy" {
if t != string(api.ServiceKindConnectProxy) {
return fmt.Errorf("unexpected ProxyType %q", t)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ func TestConstructor(t *testing.T) {
expectedErrMsg: "cannot parse 'FilterEnforced', -1 overflows uint",
ok: false,
},
"invalid proxy type": {
arguments: makeArguments(map[string]interface{}{
"ProxyType": "invalid",
"FillInterval": 30,
"MaxTokens": 20,
"TokensPerFill": 5,
}),
expectedErrMsg: `unexpected ProxyType "invalid"`,
ok: false,
},
"default proxy type": {
arguments: makeArguments(map[string]interface{}{
"FillInterval": 30,
"MaxTokens": 20,
"TokensPerFill": 5,
}),
expected: ratelimit{
ProxyType: "connect-proxy",
MaxTokens: intPointer(20),
FillInterval: intPointer(30),
TokensPerFill: intPointer(5),
},
ok: true,
},
"valid everything": {
arguments: makeArguments(map[string]interface{}{
"ProxyType": "connect-proxy",
Expand Down
5 changes: 4 additions & 1 deletion agent/envoyextensions/builtin/lua/lua.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func (l *lua) fromArguments(args map[string]interface{}) error {
if err := mapstructure.Decode(args, l); err != nil {
return fmt.Errorf("error decoding extension arguments: %v", err)
}
if l.ProxyType == "" {
l.ProxyType = string(api.ServiceKindConnectProxy)
}
return l.validate()
}

Expand All @@ -53,7 +56,7 @@ func (l *lua) validate() error {
if l.Script == "" {
resultErr = multierror.Append(resultErr, fmt.Errorf("missing Script value"))
}
if l.ProxyType != "connect-proxy" {
if l.ProxyType != string(api.ServiceKindConnectProxy) {
resultErr = multierror.Append(resultErr, fmt.Errorf("unexpected ProxyType %q", l.ProxyType))
}
if l.Listener != "inbound" && l.Listener != "outbound" {
Expand Down
9 changes: 9 additions & 0 deletions agent/envoyextensions/builtin/lua/lua_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ func TestConstructor(t *testing.T) {
arguments: makeArguments(map[string]interface{}{"Listener": "invalid"}),
ok: false,
},
"default proxy type": {
arguments: makeArguments(map[string]interface{}{"ProxyType": ""}),
expected: lua{
ProxyType: "connect-proxy",
Listener: "inbound",
Script: "lua-script",
},
ok: true,
},
"valid everything": {
arguments: makeArguments(map[string]interface{}{}),
expected: lua{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ func (p *propertyOverride) validate() error {
}
}

if p.ProxyType == "" {
p.ProxyType = api.ServiceKindConnectProxy
}
if err := validProxyTypes.CheckRequired(string(p.ProxyType), "ProxyType"); err != nil {
resultErr = multierror.Append(resultErr, err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func TestConstructor(t *testing.T) {
// enforces expected behavior until we do. Multi-member slices should be unaffected
// by WeakDecode as it is a more-permissive version of the default behavior.
"single value Patches decoded as map construction succeeds": {
arguments: makeArguments(map[string]any{"Patches": makePatch(map[string]any{})}),
arguments: makeArguments(map[string]any{"Patches": makePatch(map[string]any{}), "ProxyType": nil}),
expected: validTestCase(OpAdd, extensioncommon.TrafficDirectionOutbound, ResourceTypeRoute).expected,
ok: true,
},
Expand Down
1 change: 1 addition & 0 deletions agent/proxycfg/mesh_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/hashicorp/go-hclog"

"github.com/hashicorp/consul/acl"

cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/proxycfg/internal/watch"
"github.com/hashicorp/consul/agent/structs"
Expand Down
13 changes: 13 additions & 0 deletions agent/xds/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,19 @@ func getAPIGatewayGoldenTestCases(t *testing.T) []goldenTestCase {
Kind: structs.HTTPRoute,
Name: "route",
Rules: []structs.HTTPRouteRule{{
Filters: structs.HTTPFilters{
Headers: []structs.HTTPHeaderFilter{
{
Add: map[string]string{
"X-Header-Add": "added",
},
Set: map[string]string{
"X-Header-Set": "set",
},
Remove: []string{"X-Header-Remove"},
},
},
},
Services: []structs.HTTPService{{
Name: "service",
}},
Expand Down
12 changes: 0 additions & 12 deletions agent/xds/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,6 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot
return nil, err
}

addHeaderFiltersToVirtualHost(&reformatedRoute, virtualHost)

defaultRoute.VirtualHosts = append(defaultRoute.VirtualHosts, virtualHost)
}

Expand Down Expand Up @@ -1097,16 +1095,6 @@ func injectHeaderManipToRoute(dest *structs.ServiceRouteDestination, r *envoy_ro
return nil
}

func addHeaderFiltersToVirtualHost(dest *structs.HTTPRouteConfigEntry, vh *envoy_route_v3.VirtualHost) {
for _, rule := range dest.Rules {
for _, header := range rule.Filters.Headers {
vh.RequestHeadersToAdd = append(vh.RequestHeadersToAdd, makeHeadersValueOptions(header.Add, true)...)
vh.RequestHeadersToAdd = append(vh.RequestHeadersToAdd, makeHeadersValueOptions(header.Set, false)...)
vh.RequestHeadersToRemove = append(vh.RequestHeadersToRemove, header.Remove...)
}
}
}

func injectHeaderManipToVirtualHost(dest *structs.IngressService, vh *envoy_route_v3.VirtualHost) error {
if !dest.RequestHeaders.IsZero() {
vh.RequestHeadersToAdd = append(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
{
"versionInfo": "00000001",
"resources": [
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"name": "8080",
"virtualHosts": [
"@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"name": "8080",
"virtualHosts": [
{
"name": "api-gateway-listener-9b9265b",
"domains": [
"name": "api-gateway-listener-9b9265b",
"domains": [
"*",
"*:8080"
],
"routes": [
"routes": [
{
"match": {
"prefix": "/"
"match": {
"prefix": "/"
},
"route": {
"cluster": "service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
"route": {
"cluster": "service.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
},
"requestHeadersToAdd": [
{
"header": {
"key": "X-Header-Add",
"value": "added"
},
"append": true
},
{
"header": {
"key": "X-Header-Set",
"value": "set"
},
"append": false
}
],
"requestHeadersToRemove": [
"X-Header-Remove"
]
}
]
}
],
"validateClusters": true
"validateClusters": true
}
],
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"nonce": "00000001"
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"nonce": "00000001"
}
2 changes: 1 addition & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ replace github.com/hashicorp/consul/sdk => ../sdk

require (
github.com/google/go-cmp v0.5.9
github.com/hashicorp/consul/sdk v0.13.1
github.com/hashicorp/consul/sdk v0.14.0-rc1
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v1.5.0
github.com/hashicorp/go-rootcerts v1.0.2
Expand Down
2 changes: 2 additions & 0 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hashicorp/consul/sdk v0.14.0-rc1 h1:PuETOfN0uxl28i0Pq6rK7TBCrIl7psMbL0YTSje4KvM=
github.com/hashicorp/consul/sdk v0.14.0-rc1/go.mod h1:gHYeuDa0+0qRAD6Wwr6yznMBvBwHKoxSBoW5l73+saE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
Expand Down
Loading

0 comments on commit 751e11b

Please sign in to comment.