Enhancement: Consul Compatibility Checking (#15818)

* add functions for returning the max and min Envoy major versions
- added an UnsupportedEnvoyVersions list
- removed an unused error from TestDetermineSupportedProxyFeaturesFromString
- modified minSupportedVersion to use the function for getting the Min Envoy major version. Using just the major version without the patch is equivalent to using `.0`

* added a function for executing the envoy --version command
- added a new exec.go file to not be locked to unix system

* added envoy version check when using consul connect envoy

* added changelog entry

* added docs change
This commit is contained in:
Michael Wilkerson 2022-12-20 09:58:19 -08:00 committed by GitHub
parent e25f7313e4
commit ebed9e048f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 350 additions and 22 deletions

6
.changelog/15818.txt Normal file
View File

@ -0,0 +1,6 @@
```release-note:enhancement
connect: for early awareness of Envoy incompatibilities, when using the `consul connect envoy` command the Envoy version will now be checked for compatibility. If incompatible Consul will error and exit.
```
```release-note:breaking-change
connect: Consul will now error and exit when using the `consul connect envoy` command if the Envoy version is incompatible. To ignore this check use flag `--ignore-envoy-compatibility`
```

View File

@ -4,6 +4,7 @@ import (
"fmt"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/hashicorp/consul/agent/xds/proxysupport"
"github.com/hashicorp/go-version"
)
@ -11,7 +12,7 @@ import (
var (
// minSupportedVersion is the oldest mainline version we support. This should always be
// the zero'th point release of the last element of proxysupport.EnvoyVersions.
minSupportedVersion = version.Must(version.NewVersion("1.21.0"))
minSupportedVersion = version.Must(version.NewVersion(proxysupport.GetMinEnvoyMinorVersion()))
specificUnsupportedVersions = []unsupportedVersion{}
)

View File

@ -68,7 +68,6 @@ func TestDetermineEnvoyVersionFromNode(t *testing.T) {
func TestDetermineSupportedProxyFeaturesFromString(t *testing.T) {
const (
err1_13 = "is too old of a point release and is not supported by Consul because it does not support RBAC rules using url_path. Please upgrade to version 1.13.1+."
errTooOld = "is too old and is not supported by Consul"
)

View File

@ -1,5 +1,7 @@
package proxysupport
import "strings"
// EnvoyVersions lists the latest officially supported versions of envoy.
//
// This list must be sorted by semver descending. Only one point release for
@ -12,3 +14,28 @@ var EnvoyVersions = []string{
"1.22.5",
"1.21.5",
}
// UnsupportedEnvoyVersions lists any unsupported Envoy versions (mainly minor versions) that fall
// within the range of EnvoyVersions above.
// For example, if patch 1.21.3 (patch 3) had a breaking change, and was not supported
// even though 1.21 is a supported major release, you would then add 1.21.3 to this list.
// This list will be empty in most cases.
//
// see: https://www.consul.io/docs/connect/proxies/envoy#supported-versions
var UnsupportedEnvoyVersions = []string{}
// GetMaxEnvoyMinorVersion grabs the first value in EnvoyVersions and strips the patch number off in order
// to return the maximum supported Envoy minor version
// For example, if the input string is "1.14.1", the function would return "1.14".
func GetMaxEnvoyMinorVersion() string {
s := strings.Split(EnvoyVersions[0], ".")
return s[0] + "." + s[1]
}
// GetMinEnvoyMinorVersion grabs the last value in EnvoyVersions and strips the patch number off in order
// to return the minimum supported Envoy minor version
// For example, if the input string is "1.12.1", the function would return "1.12".
func GetMinEnvoyMinorVersion() string {
s := strings.Split(EnvoyVersions[len(EnvoyVersions)-1], ".")
return s[0] + "." + s[1]
}

View File

@ -0,0 +1,30 @@
package proxysupport
import (
"sort"
"testing"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
func TestProxySupportOrder(t *testing.T) {
versions := make([]*version.Version, len(EnvoyVersions))
beforeSort := make([]*version.Version, len(EnvoyVersions))
for i, raw := range EnvoyVersions {
v, _ := version.NewVersion(raw)
versions[i] = v
beforeSort[i] = v
}
// After this, the versions are properly sorted
// go-version has a collection container, but it only allows for sorting in ascending order
sort.Slice(versions, func(i, j int) bool {
return versions[j].LessThan(versions[i])
})
// Check that we already have a sorted list
for i := range EnvoyVersions {
assert.True(t, versions[i].Equal(beforeSort[i]))
}
}

View File

@ -7,6 +7,7 @@ import (
"net"
"os"
"os/exec"
"strconv"
"strings"
"time"
@ -21,6 +22,7 @@ import (
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/go-version"
)
func New(ui cli.Ui) *cmd {
@ -39,26 +41,27 @@ type cmd struct {
client *api.Client
// flags
meshGateway bool
gateway string
proxyID string
nodeName string
sidecarFor string
adminAccessLogPath string
adminBind string
envoyBin string
bootstrap bool
disableCentralConfig bool
grpcAddr string
grpcCAFile string
grpcCAPath string
envoyVersion string
prometheusBackendPort string
prometheusScrapePath string
prometheusCAFile string
prometheusCAPath string
prometheusCertFile string
prometheusKeyFile string
meshGateway bool
gateway string
proxyID string
nodeName string
sidecarFor string
adminAccessLogPath string
adminBind string
envoyBin string
bootstrap bool
disableCentralConfig bool
grpcAddr string
grpcCAFile string
grpcCAPath string
envoyVersion string
prometheusBackendPort string
prometheusScrapePath string
prometheusCAFile string
prometheusCAPath string
prometheusCertFile string
prometheusKeyFile string
ignoreEnvoyCompatibility bool
// mesh gateway registration information
register bool
@ -204,6 +207,10 @@ func (c *cmd) init() {
c.flags.StringVar(&c.prometheusKeyFile, "prometheus-key-file", "",
"Path to a private key file for Envoy to use when serving TLS on the Prometheus metrics endpoint. "+
"Only applicable when envoy_prometheus_bind_addr is set in proxy config.")
c.flags.BoolVar(&c.ignoreEnvoyCompatibility, "ignore-envoy-compatibility", false,
"If set to `true`, this flag ignores the Envoy version compatibility check. We recommend setting this "+
"flag to `false` to ensure compatibility with Envoy and prevent potential issues. "+
"Default is `false`.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
@ -455,6 +462,27 @@ func (c *cmd) run(args []string) int {
return 1
}
// Check if envoy version is supported
if !c.ignoreEnvoyCompatibility {
v, err := execEnvoyVersion(binary)
if err != nil {
c.UI.Warn("Couldn't get envoy version for compatibility check: " + err.Error())
return 1
}
ok, err := checkEnvoyVersionCompatibility(v, proxysupport.UnsupportedEnvoyVersions)
if err != nil {
c.UI.Warn("There was an error checking the compatibility of the envoy version: " + err.Error())
} else if !ok {
c.UI.Error(fmt.Sprintf("Envoy version %s is not supported. If there is a reason you need to use "+
"this version of envoy use the ignore-envoy-compatibility flag. Using an unsupported version of Envoy "+
"is not recommended and your experience may vary. For more information on compatibility "+
"see https://developer.hashicorp.com/consul/docs/connect/proxies/envoy#envoy-and-consul-client-agent", v))
return 1
}
}
err = execEnvoy(binary, nil, args, bootstrapJson)
if err == errUnsupportedOS {
c.UI.Error("Directly running Envoy is only supported on linux and macOS " +
@ -834,3 +862,35 @@ Usage: consul connect envoy [options] [-- pass-through options]
$ consul connect envoy -sidecar-for web -- --log-level debug
`
)
func checkEnvoyVersionCompatibility(envoyVersion string, unsupportedList []string) (bool, error) {
// Now compare the versions to the list of supported versions
v, err := version.NewVersion(envoyVersion)
if err != nil {
return false, err
}
var cs strings.Builder
// Add one to the max minor version so that we accept all patches
splitS := strings.Split(proxysupport.GetMaxEnvoyMinorVersion(), ".")
minor, err := strconv.Atoi(splitS[1])
if err != nil {
return false, err
}
minor++
maxSupported := fmt.Sprintf("%s.%d", splitS[0], minor)
// Build the constraint string, make sure that we are less than but not equal to maxSupported since we added 1
cs.WriteString(fmt.Sprintf(">= %s, < %s", proxysupport.GetMinEnvoyMinorVersion(), maxSupported))
for _, s := range unsupportedList {
cs.WriteString(fmt.Sprintf(", != %s", s))
}
constraints, err := version.NewConstraint(cs.String())
if err != nil {
return false, err
}
return constraints.Check(v), nil
}

View File

@ -3,15 +3,18 @@ package envoy
import (
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/hashicorp/consul/agent/xds/proxysupport"
"github.com/stretchr/testify/assert"
"github.com/mitchellh/cli"
@ -1522,3 +1525,83 @@ func testMockAgentSelf(wantXDSPorts agent.GRPCPorts, agentSelf110 bool) http.Han
w.Write(selfJSON)
}
}
func TestCheckEnvoyVersionCompatibility(t *testing.T) {
tests := []struct {
name string
envoyVersion string
unsupportedList []string
expectedSupport bool
isErrorExpected bool
}{
{
name: "supported-using-proxy-support-defined",
envoyVersion: proxysupport.EnvoyVersions[1],
unsupportedList: proxysupport.UnsupportedEnvoyVersions,
expectedSupport: true,
},
{
name: "supported-at-max",
envoyVersion: proxysupport.GetMaxEnvoyMinorVersion(),
unsupportedList: proxysupport.UnsupportedEnvoyVersions,
expectedSupport: true,
},
{
name: "supported-patch-higher",
envoyVersion: addNPatchVersion(proxysupport.EnvoyVersions[0], 1),
unsupportedList: proxysupport.UnsupportedEnvoyVersions,
expectedSupport: true,
},
{
name: "not-supported-minor-higher",
envoyVersion: addNMinorVersion(proxysupport.EnvoyVersions[0], 1),
unsupportedList: proxysupport.UnsupportedEnvoyVersions,
expectedSupport: false,
},
{
name: "not-supported-minor-lower",
envoyVersion: addNMinorVersion(proxysupport.EnvoyVersions[len(proxysupport.EnvoyVersions)-1], -1),
unsupportedList: proxysupport.UnsupportedEnvoyVersions,
expectedSupport: false,
},
{
name: "not-supported-explicitly-unsupported-version",
envoyVersion: addNPatchVersion(proxysupport.EnvoyVersions[0], 1),
unsupportedList: []string{"1.23.1", addNPatchVersion(proxysupport.EnvoyVersions[0], 1)},
expectedSupport: false,
},
{
name: "error-bad-input",
envoyVersion: "1.abc.3",
unsupportedList: proxysupport.UnsupportedEnvoyVersions,
expectedSupport: false,
isErrorExpected: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual, err := checkEnvoyVersionCompatibility(tc.envoyVersion, tc.unsupportedList)
if tc.isErrorExpected {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tc.expectedSupport, actual)
})
}
}
func addNPatchVersion(s string, n int) string {
splitS := strings.Split(s, ".")
minor, _ := strconv.Atoi(splitS[2])
minor += n
return fmt.Sprintf("%s.%s.%d", splitS[0], splitS[1], minor)
}
func addNMinorVersion(s string, n int) string {
splitS := strings.Split(s, ".")
major, _ := strconv.Atoi(splitS[1])
major += n
return fmt.Sprintf("%s.%d.%s", splitS[0], major, splitS[2])
}

View File

@ -0,0 +1,44 @@
package envoy
import (
"errors"
"os/exec"
"regexp"
)
const (
envoyVersionFlag = "--version"
)
// execCommand lets us mock out the exec.Command function
var execCommand = exec.Command
func execEnvoyVersion(binary string) (string, error) {
cmd := execCommand(binary, envoyVersionFlag)
output, err := cmd.Output()
if err != nil {
return "", err
}
version, err := parseEnvoyVersionNumber(string(output))
if err != nil {
return "", err
}
return version, nil
}
func parseEnvoyVersionNumber(fullVersion string) (string, error) {
// Use a regular expression to match the major.minor.patch version string in the fullVersion
// Example input:
// `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/1.24.1/Distribution/RELEASE/BoringSSL`
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
matches := re.FindStringSubmatch(fullVersion)
// If no matches were found, return an error
if len(matches) == 0 {
return "", errors.New("unable to parse Envoy version from output")
}
// Return the first match (the major.minor.patch version string)
return matches[0], nil
}

View File

@ -14,6 +14,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -133,6 +134,79 @@ func TestExecEnvoy(t *testing.T) {
}
}
func TestExecEnvoyVersion(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
tests := []struct {
name string
cmdOutput string
expectedOutput string
}{
{
name: "actual-version-output-1-24-1",
cmdOutput: `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/1.24.1/Distribution/RELEASE/BoringSSL`,
expectedOutput: "1.24.1",
},
{
name: "format-change",
cmdOutput: `envoy version: (69958e4fe32da561376d8b1d367b5e6942dfba24)__(1.24.1)/Distribution/RELEASE/BoringSSL`,
expectedOutput: "1.24.1",
},
{
name: "zeroes",
cmdOutput: `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/0.0.0/Distribution/RELEASE/BoringSSL`,
expectedOutput: "0.0.0",
},
{
name: "test-multi-digit",
cmdOutput: `envoy version: 69958e4fe32da561376d8b1d367b5e6942dfba24/1246390.9401081.1238495/Distribution/RELEASE/BoringSSL`,
expectedOutput: "1246390.9401081.1238495",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fe := fakeEnvoy{
desiredOutput: tc.cmdOutput,
}
execCommand = fe.ExecCommand
// Reset back to base exec.Command
defer func() { execCommand = exec.Command }()
version, err := execEnvoyVersion("fake-envoy")
require.NoError(t, err)
assert.Equal(t, tc.expectedOutput, version)
})
}
}
type fakeEnvoy struct {
desiredOutput string
}
func (fe fakeEnvoy) ExecCommand(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestEnvoyExecHelperProcess", "--", command}
cs = append(cs, args...)
// last argument will be the output
cs = append(cs, fe.desiredOutput)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
func TestEnvoyExecHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
output := os.Args[len(os.Args)-1]
fmt.Fprint(os.Stdout, output)
os.Exit(0)
}
type FakeEnvoyExecData struct {
Args []string `json:"args"`
ConfigPath string `json:"configPath"`

View File

@ -104,6 +104,10 @@ Usage: `consul connect envoy [options] [-- pass-through options]`
TLS on the Prometheus metrics endpoint. Only applicable when `envoy_prometheus_bind_addr`
is set in proxy config.
- `-ignore-envoy-compatibility` - If set to `true`, this flag ignores the Envoy version
compatibility check. We recommend setting this flag to `false` to ensure
compatibility with Envoy and prevent potential issues. Default is `false`.
- `-- [pass-through options]` - Any options given after a double dash are passed
directly through to the `envoy` invocation. See [Envoy's
documentation](https://www.envoyproxy.io/docs) for more details. The command