Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Test Framework] Expand capability of Echo component #13175

Merged
merged 3 commits into from Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 68 additions & 12 deletions pkg/test/application/echo/client.go
Expand Up @@ -16,8 +16,13 @@ package echo

import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"testing"

"github.com/hashicorp/go-multierror"

"google.golang.org/grpc"

Expand Down Expand Up @@ -113,9 +118,69 @@ func (r ParsedResponses) Len() int {
return len(r)
}

// IsOK indicates whether or not the first response was successful.
func (r ParsedResponses) IsOK() bool {
return r.Len() > 0 && r[0].IsOK()
func (r ParsedResponses) Check(check func(int, *ParsedResponse) error) (err error) {
if r.Len() == 0 {
return fmt.Errorf("no responses received")
}

for i, response := range r {
if e := check(i, response); e != nil {
err = multierror.Append(err, e)
}
}
return
}

func (r ParsedResponses) CheckOrFail(t testing.TB, check func(int, *ParsedResponse) error) {
if err := r.Check(check); err != nil {
t.Fatal(err)
}
}

func (r ParsedResponses) CheckOK() error {
return r.Check(func(i int, response *ParsedResponse) error {
if !response.IsOK() {
return fmt.Errorf("response[%d] Status Code: %s", i, response.Code)
}
return nil
})
}

func (r ParsedResponses) CheckOKOrFail(t testing.TB) {
if err := r.CheckOK(); err != nil {
t.Fatal(err)
}
}

func (r ParsedResponses) CheckHost(expected string) error {
return r.Check(func(i int, response *ParsedResponse) error {
if response.Host != expected {
return fmt.Errorf("response[%d] Host: expected %s, received %s", i, expected, response.Host)
}
return nil
})
}

func (r ParsedResponses) CheckHostOrFail(t testing.TB, expected string) {
if err := r.CheckHost(expected); err != nil {
t.Fatal(err)
}
}

func (r ParsedResponses) CheckPort(expected int) error {
expectedStr := strconv.Itoa(expected)
return r.Check(func(i int, response *ParsedResponse) error {
if response.Port != expectedStr {
return fmt.Errorf("response[%d] Port: expected %s, received %s", i, expectedStr, response.Port)
}
return nil
})
}

func (r ParsedResponses) CheckPortOrFail(t testing.TB, expected int) {
if err := r.CheckPort(expected); err != nil {
t.Fatal(err)
}
}

// Count occurrences of the given text within the bodies of all responses.
Expand All @@ -127,15 +192,6 @@ func (r ParsedResponses) Count(text string) int {
return count
}

// Body concatenates the bodies of all responses.
func (r ParsedResponses) Body() string {
body := ""
for _, c := range r {
body += c.Body
}
return body
}

func parseForwardedResponse(resp *proto.ForwardEchoResponse) ParsedResponses {
responses := make([]*ParsedResponse, len(resp.Output))
for i, output := range resp.Output {
Expand Down
2 changes: 1 addition & 1 deletion pkg/test/deployment/helm.go
Expand Up @@ -162,7 +162,7 @@ func HelmTemplate(deploymentName, namespace, chartDir, workDir, valuesFile strin

func exec(cmd string) (string, error) {
scopes.CI.Infof("executing: %s", cmd)
str, err := shell.Execute(cmd)
str, err := shell.Execute(true, cmd)
if err != nil {
err = errors.Wrapf(err, "error (%s) executing command: %s", str, cmd)
scopes.CI.Errorf("%v", err)
Expand Down
101 changes: 25 additions & 76 deletions pkg/test/envoy/admin_util.go
Expand Up @@ -21,100 +21,49 @@ import (
"net/http"
"time"

envoy_admin_v2alpha "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha"
routeapi "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
"github.com/gogo/protobuf/jsonpb"
"github.com/gogo/protobuf/types"
"istio.io/istio/pkg/test/util/retry"

"istio.io/istio/istioctl/pkg/util/configdump"
)
envoyAdmin "github.com/envoyproxy/go-control-plane/envoy/admin/v2alpha"
routeApi "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"

// HealthCheckState represents a health checking state returned from /server_info
type HealthCheckState string
"github.com/gogo/protobuf/jsonpb"

const (
// HealthCheckLive indicates Envoy is live and ready to serve requests
HealthCheckLive HealthCheckState = "LIVE"
"istio.io/istio/istioctl/pkg/util/configdump"
)

const (
healthCheckTimeout = 10 * time.Second
healthCheckInterval = 100 * time.Millisecond
)

var (
nilServerInfo = ServerInfo{}
)

// ServerInfo is the result of a request to /server_info
type ServerInfo struct {
ProcessName string
CompiledSHABuildType string
HealthCheckState HealthCheckState
CurrentHotRestartEpochUptime time.Duration
TotalUptime time.Duration
CurrentHotRestartEpoch int
}

// GetServerInfo returns a structure representing a call to /server_info
func GetServerInfo(adminPort int) (ServerInfo, error) {
func GetServerInfo(adminPort int) (*envoyAdmin.ServerInfo, error) {
buffer, err := doEnvoyGet("server_info", adminPort)
if err != nil {
return nilServerInfo, err
return nil, err
}

msg := &envoy_admin_v2alpha.ServerInfo{}
msg := &envoyAdmin.ServerInfo{}
if err := jsonpb.Unmarshal(buffer, msg); err != nil {
return nilServerInfo, err
}

currentHotRestartEpochUptime, err := types.DurationFromProto(msg.UptimeCurrentEpoch)
if err != nil {
return nilServerInfo, err
}

totalUptime, err := types.DurationFromProto(msg.UptimeAllEpochs)
if err != nil {
return nilServerInfo, err
}

currentEpoch := 0
if msg.CommandLineOptions != nil {
currentEpoch = int(msg.CommandLineOptions.RestartEpoch)
return nil, err
}

return ServerInfo{
ProcessName: "envoy",
CompiledSHABuildType: msg.Version,
HealthCheckState: HealthCheckState(msg.State.String()),
CurrentHotRestartEpochUptime: currentHotRestartEpochUptime,
TotalUptime: totalUptime,
CurrentHotRestartEpoch: currentEpoch,
}, nil
return msg, nil
}

// WaitForHealthCheckLive polls the server info for Envoy and waits for it to transition to "live".
func WaitForHealthCheckLive(adminPort int) error {
endTime := time.Now().Add(healthCheckTimeout)
for {
var info ServerInfo
return retry.UntilSuccess(func() error {
info, err := GetServerInfo(adminPort)
if err == nil {
if info.HealthCheckState == HealthCheckLive {
// It's running, we can return now.
return nil
}
}

// Stop trying after the timeout
if time.Now().After(endTime) {
err = fmt.Errorf("failed to start envoy after %ds. Error: %v", healthCheckTimeout/time.Second, err)
if err != nil {
return err
}

// Sleep a short before retry.
time.Sleep(healthCheckInterval)
}
if info.State != envoyAdmin.ServerInfo_LIVE {
return fmt.Errorf("envoy not live. Server State: %s", info.State)
}
return nil
}, retry.Delay(healthCheckInterval), retry.Timeout(healthCheckTimeout))
}

// GetConfigDumpStr polls Envoy admin port for the config dump and returns the response as a string.
Expand All @@ -127,13 +76,13 @@ func GetConfigDumpStr(adminPort int) (string, error) {
}

// GetConfigDump polls Envoy admin port for the config dump and returns the response.
func GetConfigDump(adminPort int) (*envoy_admin_v2alpha.ConfigDump, error) {
func GetConfigDump(adminPort int) (*envoyAdmin.ConfigDump, error) {
buffer, err := doEnvoyGet("config_dump", adminPort)
if err != nil {
return nil, err
}

msg := &envoy_admin_v2alpha.ConfigDump{}
msg := &envoyAdmin.ConfigDump{}
if err := jsonpb.Unmarshal(buffer, msg); err != nil {
return nil, err
}
Expand All @@ -150,7 +99,7 @@ func doEnvoyGet(path string, adminPort int) (*bytes.Buffer, error) {
}

// IsClusterPresent inspects the given Envoy config dump, looking for the given cluster
func IsClusterPresent(cfg *envoy_admin_v2alpha.ConfigDump, clusterName string) bool {
func IsClusterPresent(cfg *envoyAdmin.ConfigDump, clusterName string) bool {
wrapper := configdump.Wrapper{ConfigDump: cfg}
clusters, err := wrapper.GetClusterConfigDump()
if err != nil {
Expand All @@ -169,7 +118,7 @@ func IsClusterPresent(cfg *envoy_admin_v2alpha.ConfigDump, clusterName string) b
}

// IsOutboundListenerPresent inspects the given Envoy config dump, looking for the given listener.
func IsOutboundListenerPresent(cfg *envoy_admin_v2alpha.ConfigDump, listenerName string) bool {
func IsOutboundListenerPresent(cfg *envoyAdmin.ConfigDump, listenerName string) bool {
wrapper := configdump.Wrapper{ConfigDump: cfg}
listeners, err := wrapper.GetListenerConfigDump()
if err != nil {
Expand All @@ -185,7 +134,7 @@ func IsOutboundListenerPresent(cfg *envoy_admin_v2alpha.ConfigDump, listenerName
}

// IsOutboundRoutePresent inspects the given Envoy config dump, looking for an outbound route which targets the given cluster.
func IsOutboundRoutePresent(cfg *envoy_admin_v2alpha.ConfigDump, clusterName string) bool {
func IsOutboundRoutePresent(cfg *envoyAdmin.ConfigDump, clusterName string) bool {
wrapper := configdump.Wrapper{ConfigDump: cfg}
routes, err := wrapper.GetRouteConfigDump()
if err != nil {
Expand All @@ -197,12 +146,12 @@ func IsOutboundRoutePresent(cfg *envoy_admin_v2alpha.ConfigDump, clusterName str
if r.RouteConfig != nil {
for _, vh := range r.RouteConfig.VirtualHosts {
for _, route := range vh.Routes {
actionRoute, ok := route.Action.(*routeapi.Route_Route)
actionRoute, ok := route.Action.(*routeApi.Route_Route)
if !ok {
continue
}

cluster, ok := actionRoute.Route.ClusterSpecifier.(*routeapi.RouteAction_Cluster)
cluster, ok := actionRoute.Route.ClusterSpecifier.(*routeApi.RouteAction_Cluster)
if !ok {
continue
}
Expand All @@ -222,7 +171,7 @@ func doHTTPGet(requestURL string) (*bytes.Buffer, error) {
if err != nil {
return nil, err
}
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()

if response.StatusCode != 200 {
return nil, fmt.Errorf("unexpected status %d", response.StatusCode)
Expand Down
59 changes: 59 additions & 0 deletions pkg/test/framework/components/echo/call.go
@@ -0,0 +1,59 @@
// Copyright 2019 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package echo

import "net/http"

// CallProtocol enumerates the protocol options for calling an Endpoint endpoint.
type CallProtocol string

const (
HTTP CallProtocol = "http"
HTTPS CallProtocol = "https"
GRPC CallProtocol = "grpc"
GRPCS CallProtocol = "grpcs"
WebSocket CallProtocol = "ws"
WebSocketS CallProtocol = "wss"
)

// CallOptions defines options for calling a Endpoint.
type CallOptions struct {
// Target instance of the call. Required.
Target Instance

// Port on the target Instance. Either Port or PortName must be specified.
Port *Port

// PortName of the port on the target Instance. Either Port or PortName must be specified.
PortName string

// Protocol to be used when making the call. If not provided, the protocol of the port
// will be used.
Protocol CallProtocol

// Host specifies the host to be used on the request. If not provided, an appropriate
// default is chosen for the target Instance.
Host string

// Path specifies the URL path for the request.
Path string

// Count indicates the number of exchanges that should be made with the service endpoint.
// If Count <= 0, defaults to 1.
Count int

// Headers indicates headers that should be sent in the request. Ignored for WebSocket calls.
Headers http.Header
}