Skip to content

Commit

Permalink
Enhancement: Consul Compatibility Checking (#15818)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wilkermichael authored Dec 20, 2022
1 parent 74b11c4 commit 1b28b89
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changelog/15818.txt
Original file line number Diff line number Diff line change
@@ -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`
```
3 changes: 2 additions & 1 deletion agent/xds/envoy_versioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ 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"
)

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{}
)
Expand Down
1 change: 0 additions & 1 deletion agent/xds/envoy_versioning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
27 changes: 27 additions & 0 deletions agent/xds/proxysupport/proxysupport.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]
}
30 changes: 30 additions & 0 deletions agent/xds/proxysupport/proxysupport_test.go
Original file line number Diff line number Diff line change
@@ -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]))
}
}
100 changes: 80 additions & 20 deletions command/connect/envoy/envoy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net"
"os"
"os/exec"
"strconv"
"strings"
"time"

Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 " +
Expand Down Expand Up @@ -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
}
83 changes: 83 additions & 0 deletions command/connect/envoy/envoy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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])
}
44 changes: 44 additions & 0 deletions command/connect/envoy/exec.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 1b28b89

Please sign in to comment.