integrationtests: check for stat output of policy resources (#6890)

This PR adds a new `policy` integration tests suite
to check the output of for the `srv` and `saz` commands and
make sure they have the expected format.

This is majorly needed to make sure things work as expected
with stat cmd output without having to do manual testing.

Signed-off-by: Tarun Pothulapati <tarunpothulapati@outlook.com>
This commit is contained in:
Tarun Pothulapati 2021-09-17 16:37:44 +05:30 committed by GitHub
parent d9470b3b35
commit 174c50a235
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 547 additions and 0 deletions

View File

@ -0,0 +1,244 @@
package policy
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/linkerd/linkerd2/testutil"
)
var TestHelper *testutil.TestHelper
func TestMain(m *testing.M) {
TestHelper = testutil.NewTestHelper()
os.Exit(m.Run())
}
//////////////////////
/// TEST EXECUTION ///
//////////////////////
func TestPolicy(t *testing.T) {
ctx := context.Background()
// Test authorization stats
TestHelper.WithDataPlaneNamespace(ctx, "stat-authz-test", map[string]string{}, t, func(t *testing.T, prefixedNs string) {
emojivotoYaml, err := testutil.ReadFile("testdata/emojivoto.yaml")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to read emojivoto yaml",
"failed to read emojivoto yaml\n%s\n", err)
}
emojivotoYaml = strings.ReplaceAll(emojivotoYaml, "___NS___", prefixedNs)
out, stderr, err := TestHelper.PipeToLinkerdRun(emojivotoYaml, "inject", "-")
if err != nil {
testutil.AnnotatedFatalf(t, "'linkerd inject' command failed",
"'linkerd inject' command failed\n%s\n%s", out, stderr)
}
out, err = TestHelper.KubectlApply(out, prefixedNs)
if err != nil {
testutil.AnnotatedFatalf(t, "failed to apply emojivoto resources",
"failed to apply emojivoto resources: %s\n %s", err, out)
}
emojivotoPolicy, err := testutil.ReadFile("testdata/emoji-policy.yaml")
if err != nil {
testutil.AnnotatedFatalf(t, "failed to read emoji-policy yaml",
"failed to read emoji-policy yaml\n%s\n", err)
}
out, err = TestHelper.KubectlApply(emojivotoPolicy, prefixedNs)
if err != nil {
testutil.AnnotatedFatalf(t, "failed to apply emojivoto policy resources",
"failed to apply emojivoto policy resources: %s\n %s", err, out)
}
// wait for deployments to start
for _, deploy := range []string{"web", "emoji", "vote-bot", "voting"} {
if err := TestHelper.CheckPods(ctx, prefixedNs, deploy, 1); err != nil {
if rce, ok := err.(*testutil.RestartCountError); ok {
testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
} else {
testutil.AnnotatedError(t, "CheckPods timed-out", err)
}
}
}
testCases := []struct {
args []string
expectedRows []string
isServer bool
}{
{
args: []string{"viz", "stat", "srv", "-n", prefixedNs},
expectedRows: []string{
"emoji-grpc",
"voting-grpc",
"web-http",
},
isServer: true,
},
{
args: []string{"viz", "stat", "srv/emoji-grpc", "-n", prefixedNs},
expectedRows: []string{
"emoji-grpc",
},
isServer: true,
},
{
args: []string{"viz", "stat", "saz", "-n", prefixedNs},
expectedRows: []string{
"emoji-grpc",
"voting-grpc",
"web-public",
},
isServer: false,
},
{
args: []string{"viz", "stat", "saz/emoji-grpc", "-n", prefixedNs},
expectedRows: []string{
"emoji-grpc",
},
isServer: false,
},
}
for _, tt := range testCases {
tt := tt // pin
timeout := 3 * time.Minute
t.Run("linkerd "+strings.Join(tt.args, " "), func(t *testing.T) {
err := TestHelper.RetryFor(timeout, func() error {
// Use a short time window so that transient errors at startup
// fall out of the window.
tt.args = append(tt.args, "-t", "30s")
out, err := TestHelper.LinkerdRun(tt.args...)
if err != nil {
testutil.AnnotatedFatalf(t, "unexpected stat error",
"unexpected stat error: %s\n%s", err, out)
}
var expectedColumnCount int
if tt.isServer {
expectedColumnCount = 8
} else {
expectedColumnCount = 6
}
rowStats, err := ParseAuthzRows(out, len(tt.expectedRows), expectedColumnCount, tt.isServer)
if err != nil {
return err
}
for _, name := range tt.expectedRows {
if err := validateAuthzRows(name, rowStats, tt.isServer); err != nil {
return err
}
}
return nil
})
if err != nil {
testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out checking stats (%s)", timeout), err)
}
})
}
})
}
func validateAuthzRows(name string, rowStats map[string]*testutil.RowStat, isServer bool) error {
stat, ok := rowStats[name]
if !ok {
return fmt.Errorf("No stats found for [%s]", name)
}
// Check for suffix only, as the value will not be 100% always with
// the normal emojivoto sample
if !strings.HasSuffix(stat.Success, "%") {
return fmt.Errorf("Unexpected success rate for [%s], got [%s]",
name, stat.Success)
}
if isServer {
if !strings.HasSuffix(stat.UnauthorizedRPS, "rps") {
return fmt.Errorf("Unexpected Unauthorized RPS for [%s], got [%s]",
name, stat.UnauthorizedRPS)
}
}
if !strings.HasSuffix(stat.Rps, "rps") {
return fmt.Errorf("Unexpected rps for [%s], got [%s]",
name, stat.Rps)
}
if !strings.HasSuffix(stat.P50Latency, "ms") {
return fmt.Errorf("Unexpected p50 latency for [%s], got [%s]",
name, stat.P50Latency)
}
if !strings.HasSuffix(stat.P95Latency, "ms") {
return fmt.Errorf("Unexpected p95 latency for [%s], got [%s]",
name, stat.P95Latency)
}
if !strings.HasSuffix(stat.P99Latency, "ms") {
return fmt.Errorf("Unexpected p99 latency for [%s], got [%s]",
name, stat.P99Latency)
}
if isServer {
_, err := strconv.Atoi(stat.TCPOpenConnections)
if err != nil {
return fmt.Errorf("Error parsing number of TCP connections [%s]: %s", stat.TCPOpenConnections, err.Error())
}
}
return nil
}
// ParseRows parses the output of linkerd stat on a policy resource
func ParseAuthzRows(out string, expectedRowCount, expectedColumnCount int, isServer bool) (map[string]*testutil.RowStat, error) {
rows, err := testutil.CheckRowCount(out, expectedRowCount)
if err != nil {
return nil, err
}
rowStats := make(map[string]*testutil.RowStat)
for _, row := range rows {
fields := strings.Fields(row)
if len(fields) != expectedColumnCount {
return nil, fmt.Errorf(
"Expected [%d] columns in stat output, got [%d]; full output:\n%s",
expectedColumnCount, len(fields), row)
}
i := 0
rowStats[fields[0]] = &testutil.RowStat{
Name: fields[0],
}
if isServer {
rowStats[fields[0]].UnauthorizedRPS = fields[1+i]
rowStats[fields[0]].Success = fields[2+i]
rowStats[fields[0]].Rps = fields[3+i]
rowStats[fields[0]].P50Latency = fields[4+i]
rowStats[fields[0]].P95Latency = fields[5+i]
rowStats[fields[0]].P99Latency = fields[6+i]
rowStats[fields[0]].TCPOpenConnections = fields[7+i]
} else {
rowStats[fields[0]].Success = fields[1+i]
rowStats[fields[0]].Rps = fields[2+i]
rowStats[fields[0]].P50Latency = fields[3+i]
rowStats[fields[0]].P95Latency = fields[4+i]
rowStats[fields[0]].P99Latency = fields[5+i]
}
}
return rowStats, nil
}

View File

@ -0,0 +1,97 @@
---
apiVersion: policy.linkerd.io/v1alpha1
kind: Server
metadata:
name: emoji-grpc
labels:
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/name: emoji
app.kubernetes.io/version: v11
spec:
podSelector:
matchLabels:
app: emoji-svc
port: grpc
proxyProtocol: gRPC
---
apiVersion: policy.linkerd.io/v1alpha1
kind: ServerAuthorization
metadata:
name: emoji-grpc
labels:
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/name: emoji
app.kubernetes.io/version: v11
spec:
# Allow all authenticated clients to access the (read-only) emoji service.
server:
name: emoji-grpc
client:
meshTLS:
identities:
- "*.linkerd-stat-authz-test.serviceaccount.identity.linkerd.cluster.local"
---
apiVersion: policy.linkerd.io/v1alpha1
kind: Server
metadata:
name: voting-grpc
labels:
app: voting-svc
spec:
podSelector:
matchLabels:
app: voting-svc
port: grpc
proxyProtocol: gRPC
---
apiVersion: policy.linkerd.io/v1alpha1
kind: ServerAuthorization
metadata:
name: voting-grpc
labels:
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/name: voting
app.kubernetes.io/version: v11
spec:
server:
name: voting-grpc
# The voting service only allows requests from the web service.
client:
meshTLS:
serviceAccounts:
- name: web
---
apiVersion: policy.linkerd.io/v1alpha1
kind: Server
metadata:
name: web-http
labels:
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/name: web
app.kubernetes.io/version: v11
spec:
podSelector:
matchLabels:
app: web-svc
port: http
proxyProtocol: HTTP/1
---
apiVersion: policy.linkerd.io/v1alpha1
kind: ServerAuthorization
metadata:
name: web-public
labels:
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/name: web
app.kubernetes.io/version: v11
spec:
server:
name: web-http
# Allow all clients to access the web HTTP port without regard for
# authentication. If unauthenticated connections are permitted, there is no
# need to describe authenticated clients.
client:
unauthenticated: true
networks:
- cidr: 0.0.0.0/0
- cidr: ::/0

View File

@ -0,0 +1,205 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: emoji
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: voting
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: web
---
apiVersion: v1
kind: Service
metadata:
name: emoji-svc
spec:
ports:
- name: grpc
port: 8080
targetPort: 8080
- name: prom
port: 8801
targetPort: 8801
selector:
app: emoji-svc
---
apiVersion: v1
kind: Service
metadata:
name: voting-svc
spec:
ports:
- name: grpc
port: 8080
targetPort: 8080
- name: prom
port: 8801
targetPort: 8801
selector:
app: voting-svc
---
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
ports:
- name: http
port: 80
targetPort: 8080
selector:
app: web-svc
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: emoji
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/version: v11
name: emoji
spec:
replicas: 1
selector:
matchLabels:
app: emoji-svc
version: v11
template:
metadata:
labels:
app: emoji-svc
version: v11
spec:
containers:
- env:
- name: GRPC_PORT
value: "8080"
- name: PROM_PORT
value: "8801"
image: docker.l5d.io/buoyantio/emojivoto-emoji-svc:v11
name: emoji-svc
ports:
- containerPort: 8080
name: grpc
- containerPort: 8801
name: prom
resources:
requests:
cpu: 100m
serviceAccountName: emoji
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: vote-bot
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/version: v11
name: vote-bot
spec:
replicas: 1
selector:
matchLabels:
app: vote-bot
version: v11
template:
metadata:
labels:
app: vote-bot
version: v11
spec:
containers:
- command:
- emojivoto-vote-bot
env:
- name: WEB_HOST
value: web-svc.___NS___:80
image: docker.l5d.io/buoyantio/emojivoto-web:v11
name: vote-bot
resources:
requests:
cpu: 10m
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: voting
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/version: v11
name: voting
spec:
replicas: 1
selector:
matchLabels:
app: voting-svc
version: v11
template:
metadata:
labels:
app: voting-svc
version: v11
spec:
containers:
- env:
- name: GRPC_PORT
value: "8080"
- name: PROM_PORT
value: "8801"
image: docker.l5d.io/buoyantio/emojivoto-voting-svc:v11
name: voting-svc
ports:
- containerPort: 8080
name: grpc
- containerPort: 8801
name: prom
resources:
requests:
cpu: 100m
serviceAccountName: voting
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: web
app.kubernetes.io/part-of: emojivoto
app.kubernetes.io/version: v11
name: web
spec:
replicas: 1
selector:
matchLabels:
app: web-svc
version: v11
template:
metadata:
labels:
app: web-svc
version: v11
spec:
containers:
- env:
- name: WEB_PORT
value: "8080"
- name: EMOJISVC_HOST
value: emoji-svc.___NS___:8080
- name: VOTINGSVC_HOST
value: voting-svc.___NS___:8080
- name: INDEX_BUNDLE
value: dist/index_bundle.js
image: docker.l5d.io/buoyantio/emojivoto-web:v11
name: web-svc
ports:
- containerPort: 8080
name: http
resources:
requests:
cpu: 100m
serviceAccountName: web

View File

@ -672,6 +672,7 @@ type RowStat struct {
P95Latency string
P99Latency string
TCPOpenConnections string
UnauthorizedRPS string
}
// CheckRowCount checks that expectedRowCount rows have been returned