Skip to content

Commit 73e88b3

Browse files
authored
Run integration tests against an external agent process (#552)
* Run integration tests against an external agent process * Fix lint errors
1 parent 114a680 commit 73e88b3

File tree

10 files changed

+306
-62
lines changed

10 files changed

+306
-62
lines changed

.golangci.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
run:
2+
timeout: 3m
3+
linters:
4+
disable-all: true
5+
enable:
6+
- gofmt
7+
- revive
8+
- gosec
9+
- govet
10+
- unused
11+
issues:
12+
fix: true
13+
exclude-rules:
14+
# Don't run security checks on test files
15+
- path: _test\.go
16+
linters:
17+
- gosec
18+
- path: ^tests/
19+
linters:
20+
- gosec

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ test:
6060
go test -race -covermode=atomic -coverprofile=konnectivity.out ./... && go tool cover -html=konnectivity.out -o=konnectivity.html
6161
cd konnectivity-client && go test -race -covermode=atomic -coverprofile=client.out ./... && go tool cover -html=client.out -o=client.html
6262

63+
.PHONY: test-integration
64+
test-integration: build
65+
go test -race ./tests -agent-path $(PWD)/bin/proxy-agent
66+
6367
## --------------------------------------
6468
## Binaries
6569
## --------------------------------------
@@ -91,7 +95,7 @@ bin/proxy-server: bin $(SOURCE)
9195
.PHONY: lint
9296
lint:
9397
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(INSTALL_LOCATION) v$(GOLANGCI_LINT_VERSION)
94-
$(INSTALL_LOCATION)/golangci-lint run --no-config --disable-all --enable=gofmt,revive,gosec,govet,unused --fix --verbose --timeout 3m
98+
$(INSTALL_LOCATION)/golangci-lint run --verbose
9599

96100
## --------------------------------------
97101
## Go

cmd/agent/app/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ func (a *Agent) runHealthServer(o *options.GrpcProxyAgentOptions, cs agent.Readi
132132

133133
// Always be verbose if the check has failed
134134
if len(failedChecks) > 0 {
135-
klog.V(0).Infoln("%s check failed: \n%v", strings.Join(failedChecks, ","), individualCheckOutput.String())
135+
klog.V(0).Infof("%s check failed: \n%v", strings.Join(failedChecks, ","), individualCheckOutput.String())
136136
w.WriteHeader(http.StatusServiceUnavailable)
137-
fmt.Fprintf(w, individualCheckOutput.String())
137+
fmt.Fprint(w, individualCheckOutput.String())
138138
return
139139
}
140140

pkg/server/server_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ func TestServerProxyNoBackend(t *testing.T) {
442442
}
443443
baseServerProxyTestWithoutBackend(t, validate)
444444

445-
if err := metricstest.ExpectServerDialFailure(metrics.DialFailureNoAgent, 1); err != nil {
445+
if err := metricstest.DefaultTester.ExpectServerDialFailure(metrics.DialFailureNoAgent, 1); err != nil {
446446
t.Error(err)
447447
}
448448
}
@@ -664,14 +664,14 @@ func closeRspPkt(connectID int64, errMsg string) *client.Packet {
664664

665665
func assertEstablishedConnsMetric(t testing.TB, expect int) {
666666
t.Helper()
667-
if err := metricstest.ExpectServerEstablishedConns(expect); err != nil {
667+
if err := metricstest.DefaultTester.ExpectServerEstablishedConns(expect); err != nil {
668668
t.Errorf("Expected %d %s metric: %v", expect, "established_connections", err)
669669
}
670670
}
671671

672672
func assertReadyBackendsMetric(t testing.TB, expect int) {
673673
t.Helper()
674-
if err := metricstest.ExpectServerReadyBackends(expect); err != nil {
674+
if err := metricstest.DefaultTester.ExpectServerReadyBackends(expect); err != nil {
675675
t.Errorf("Expected %d %s metric: %v", expect, "ready_backend_connections", err)
676676
}
677677
}

pkg/testing/metrics/metrics.go

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,54 +58,81 @@ const (
5858
konnectivity_network_proxy_agent_open_endpoint_connections %d`
5959
)
6060

61-
func ExpectServerDialFailures(expected map[server.DialFailureReason]int) error {
61+
var DefaultTester = &Tester{}
62+
var _ ServerTester = DefaultTester
63+
var _ AgentTester = DefaultTester
64+
65+
type Tester struct {
66+
// Endpoint is the metrics endpoint to scrape metrics from. If it is empty, the in-process
67+
// DefaultGatherer is used.
68+
Endpoint string
69+
}
70+
71+
type ServerTester interface {
72+
ExpectServerDialFailures(map[server.DialFailureReason]int) error
73+
ExpectServerDialFailure(server.DialFailureReason, int) error
74+
ExpectServerPendingDials(int) error
75+
ExpectServerReadyBackends(int) error
76+
ExpectServerEstablishedConns(int) error
77+
}
78+
79+
type AgentTester interface {
80+
ExpectAgentDialFailures(map[agent.DialFailureReason]int) error
81+
ExpectAgentDialFailure(agent.DialFailureReason, int) error
82+
ExpectAgentEndpointConnections(int) error
83+
}
84+
85+
func (t *Tester) ExpectServerDialFailures(expected map[server.DialFailureReason]int) error {
6286
expect := serverDialFailureHeader + "\n"
6387
for r, v := range expected {
6488
expect += fmt.Sprintf(serverDialFailureSample+"\n", r, v)
6589
}
66-
return ExpectMetric(server.Namespace, server.Subsystem, "dial_failure_count", expect)
90+
return t.ExpectMetric(server.Namespace, server.Subsystem, "dial_failure_count", expect)
6791
}
6892

69-
func ExpectServerDialFailure(reason server.DialFailureReason, count int) error {
70-
return ExpectServerDialFailures(map[server.DialFailureReason]int{reason: count})
93+
func (t *Tester) ExpectServerDialFailure(reason server.DialFailureReason, count int) error {
94+
return t.ExpectServerDialFailures(map[server.DialFailureReason]int{reason: count})
7195
}
7296

73-
func ExpectServerPendingDials(v int) error {
97+
func (t *Tester) ExpectServerPendingDials(v int) error {
7498
expect := serverPendingDialsHeader + "\n"
7599
expect += fmt.Sprintf(serverPendingDialsSample+"\n", v)
76-
return ExpectMetric(server.Namespace, server.Subsystem, "pending_backend_dials", expect)
100+
return t.ExpectMetric(server.Namespace, server.Subsystem, "pending_backend_dials", expect)
77101
}
78102

79-
func ExpectServerReadyBackends(v int) error {
103+
func (t *Tester) ExpectServerReadyBackends(v int) error {
80104
expect := serverReadyBackendsHeader + "\n"
81105
expect += fmt.Sprintf(serverReadyBackendsSample+"\n", v)
82-
return ExpectMetric(server.Namespace, server.Subsystem, "ready_backend_connections", expect)
106+
return t.ExpectMetric(server.Namespace, server.Subsystem, "ready_backend_connections", expect)
83107
}
84108

85-
func ExpectServerEstablishedConns(v int) error {
109+
func (t *Tester) ExpectServerEstablishedConns(v int) error {
86110
expect := serverEstablishedConnsHeader + "\n"
87111
expect += fmt.Sprintf(serverEstablishedConnsSample+"\n", v)
88-
return ExpectMetric(server.Namespace, server.Subsystem, "established_connections", expect)
112+
return t.ExpectMetric(server.Namespace, server.Subsystem, "established_connections", expect)
89113
}
90114

91-
func ExpectAgentDialFailures(expected map[agent.DialFailureReason]int) error {
115+
func (t *Tester) ExpectAgentDialFailures(expected map[agent.DialFailureReason]int) error {
92116
expect := agentDialFailureHeader + "\n"
93117
for r, v := range expected {
94118
expect += fmt.Sprintf(agentDialFailureSample+"\n", r, v)
95119
}
96-
return ExpectMetric(agent.Namespace, agent.Subsystem, "endpoint_dial_failure_total", expect)
120+
return t.ExpectMetric(agent.Namespace, agent.Subsystem, "endpoint_dial_failure_total", expect)
97121
}
98122

99-
func ExpectAgentDialFailure(reason agent.DialFailureReason, count int) error {
100-
return ExpectAgentDialFailures(map[agent.DialFailureReason]int{reason: count})
123+
func (t *Tester) ExpectAgentDialFailure(reason agent.DialFailureReason, count int) error {
124+
return t.ExpectAgentDialFailures(map[agent.DialFailureReason]int{reason: count})
101125
}
102126

103-
func ExpectAgentEndpointConnections(count int) error {
127+
func (t *Tester) ExpectAgentEndpointConnections(count int) error {
104128
expect := fmt.Sprintf(agentEndpointConnections+"\n", count)
105-
return ExpectMetric(agent.Namespace, agent.Subsystem, "open_endpoint_connections", expect)
129+
return t.ExpectMetric(agent.Namespace, agent.Subsystem, "open_endpoint_connections", expect)
106130
}
107131

108-
func ExpectMetric(namespace, subsystem, name, expected string) error {
132+
func (t *Tester) ExpectMetric(namespace, subsystem, name, expected string) error {
109133
fqName := prometheus.BuildFQName(namespace, subsystem, name)
110-
return promtest.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expected), fqName)
134+
if t.Endpoint == "" {
135+
return promtest.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expected), fqName)
136+
}
137+
return promtest.ScrapeAndCompare(t.Endpoint, strings.NewReader(expected), fqName)
111138
}

tests/framework/agent.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,24 @@ import (
2121
"fmt"
2222
"log"
2323
"net"
24+
"net/http"
25+
"os"
26+
"os/exec"
2427
"path/filepath"
2528
"strconv"
2629
"sync"
2730
"testing"
2831
"time"
2932

33+
"github.com/prometheus/client_golang/prometheus"
34+
"github.com/spf13/pflag"
3035
"k8s.io/apimachinery/pkg/util/wait"
3136

3237
agentapp "sigs.k8s.io/apiserver-network-proxy/cmd/agent/app"
3338
agentopts "sigs.k8s.io/apiserver-network-proxy/cmd/agent/app/options"
3439
"sigs.k8s.io/apiserver-network-proxy/pkg/agent"
40+
agentmetrics "sigs.k8s.io/apiserver-network-proxy/pkg/agent/metrics"
41+
metricstest "sigs.k8s.io/apiserver-network-proxy/pkg/testing/metrics"
3542
)
3643

3744
type AgentOpts struct {
@@ -47,6 +54,7 @@ type Agent interface {
4754
GetConnectedServerCount() (int, error)
4855
Ready() bool
4956
Stop()
57+
Metrics() metricstest.AgentTester
5058
}
5159
type InProcessAgentRunner struct{}
5260

@@ -106,6 +114,99 @@ func (a *inProcessAgent) Ready() bool {
106114
return checkReadiness(a.healthAddr)
107115
}
108116

117+
func (a *inProcessAgent) Metrics() metricstest.AgentTester {
118+
return metricstest.DefaultTester
119+
}
120+
121+
type ExternalAgentRunner struct {
122+
ExecutablePath string
123+
}
124+
125+
func (r *ExternalAgentRunner) Start(t testing.TB, opts AgentOpts) (Agent, error) {
126+
o, err := agentOptions(t, opts)
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
args := []string{}
132+
o.Flags().VisitAll(func(f *pflag.Flag) {
133+
args = append(args, fmt.Sprintf("--%s=%s", f.Name, f.Value.String()))
134+
})
135+
136+
cmd := exec.Command(r.ExecutablePath, args...)
137+
cmd.Stdout = os.Stdout // Forward stdout & stderr
138+
cmd.Stderr = os.Stderr
139+
if err := cmd.Start(); err != nil {
140+
return nil, err
141+
}
142+
143+
healthAddr := net.JoinHostPort(o.HealthServerHost, strconv.Itoa(o.HealthServerPort))
144+
a := &externalAgent{
145+
cmd: cmd,
146+
healthAddr: healthAddr,
147+
adminAddr: net.JoinHostPort(o.AdminBindAddress, strconv.Itoa(o.AdminServerPort)),
148+
agentID: opts.AgentID,
149+
metrics: &metricstest.Tester{Endpoint: fmt.Sprintf("http://%s/metrics", healthAddr)},
150+
}
151+
t.Cleanup(a.Stop)
152+
153+
a.waitForLiveness()
154+
return a, nil
155+
}
156+
157+
type externalAgent struct {
158+
healthAddr, adminAddr string
159+
agentID string
160+
cmd *exec.Cmd
161+
metrics *metricstest.Tester
162+
163+
stopOnce sync.Once
164+
}
165+
166+
func (a *externalAgent) Stop() {
167+
a.stopOnce.Do(func() {
168+
if err := a.cmd.Process.Kill(); err != nil {
169+
log.Fatalf("Error stopping agent process: %v", err)
170+
}
171+
})
172+
}
173+
174+
var (
175+
agentConnectedServerCountMetric = prometheus.BuildFQName(agentmetrics.Namespace, agentmetrics.Subsystem, "open_server_connections")
176+
)
177+
178+
func (a *externalAgent) GetConnectedServerCount() (int, error) {
179+
return readIntGauge(a.metrics.Endpoint, agentConnectedServerCountMetric)
180+
}
181+
182+
func (a *externalAgent) Ready() bool {
183+
resp, err := http.Get(fmt.Sprintf("http://%s/readyz", a.healthAddr))
184+
if err != nil {
185+
return false
186+
}
187+
resp.Body.Close()
188+
return resp.StatusCode == http.StatusOK
189+
}
190+
191+
func (a *externalAgent) Metrics() metricstest.AgentTester {
192+
return a.metrics
193+
}
194+
195+
func (a *externalAgent) waitForLiveness() error {
196+
err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
197+
resp, err := http.Get(fmt.Sprintf("http://%s/healthz", a.healthAddr))
198+
if err != nil {
199+
return false, nil
200+
}
201+
resp.Body.Close()
202+
return resp.StatusCode == http.StatusOK, nil
203+
})
204+
if err != nil {
205+
return fmt.Errorf("timed out waiting for agent liveness check")
206+
}
207+
return nil
208+
}
209+
109210
func agentOptions(t testing.TB, opts AgentOpts) (*agentopts.GrpcProxyAgentOptions, error) {
110211
o := agentopts.NewGrpcProxyAgentOptions()
111212

tests/framework/metrics.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package framework
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
23+
dto "github.com/prometheus/client_model/go"
24+
"github.com/prometheus/common/expfmt"
25+
)
26+
27+
func readIntGauge(metricsURL, metricName string) (int, error) {
28+
metrics, err := scrapeMetrics(metricsURL)
29+
if err != nil {
30+
return 0, err
31+
}
32+
33+
metric, found := metrics[metricName]
34+
if !found {
35+
return 0, fmt.Errorf("missing %s metric", metricName)
36+
}
37+
38+
vals := metric.GetMetric()
39+
if len(vals) == 0 {
40+
return 0, fmt.Errorf("missing value for %s metric", metricName)
41+
}
42+
43+
return int(vals[0].GetGauge().GetValue()), nil
44+
}
45+
46+
func scrapeMetrics(metricsURL string) (map[string]*dto.MetricFamily, error) {
47+
resp, err := http.Get(metricsURL)
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to scrape metrics from %s: %w", metricsURL, err)
50+
}
51+
defer resp.Body.Close()
52+
53+
if resp.StatusCode != http.StatusOK {
54+
return nil, fmt.Errorf("unexpected %q status scraping metrics from %s", resp.Status, metricsURL)
55+
}
56+
57+
var parser expfmt.TextParser
58+
mf, err := parser.TextToMetricFamilies(resp.Body)
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to parse metrics: %w", err)
61+
}
62+
return mf, nil
63+
}

0 commit comments

Comments
 (0)