Skip to content

Commit f918977

Browse files
authored
feat: add new loadtest type agentconn (#4899)
1 parent 56b963a commit f918977

File tree

5 files changed

+898
-2
lines changed

5 files changed

+898
-2
lines changed

cli/loadtestconfig.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/coder/coder/coderd/httpapi"
99
"github.com/coder/coder/codersdk"
10+
"github.com/coder/coder/loadtest/agentconn"
1011
"github.com/coder/coder/loadtest/harness"
1112
"github.com/coder/coder/loadtest/placebo"
1213
"github.com/coder/coder/loadtest/workspacebuild"
@@ -86,6 +87,7 @@ func (s LoadTestStrategy) ExecutionStrategy() harness.ExecutionStrategy {
8687
type LoadTestType string
8788

8889
const (
90+
LoadTestTypeAgentConn LoadTestType = "agentconn"
8991
LoadTestTypePlacebo LoadTestType = "placebo"
9092
LoadTestTypeWorkspaceBuild LoadTestType = "workspacebuild"
9193
)
@@ -97,6 +99,8 @@ type LoadTest struct {
9799
// the count is 0 or negative, defaults to 1.
98100
Count int `json:"count"`
99101

102+
// AgentConn must be set if type == "agentconn".
103+
AgentConn *agentconn.Config `json:"agentconn,omitempty"`
100104
// Placebo must be set if type == "placebo".
101105
Placebo *placebo.Config `json:"placebo,omitempty"`
102106
// WorkspaceBuild must be set if type == "workspacebuild".
@@ -105,17 +109,20 @@ type LoadTest struct {
105109

106110
func (t LoadTest) NewRunner(client *codersdk.Client) (harness.Runnable, error) {
107111
switch t.Type {
112+
case LoadTestTypeAgentConn:
113+
if t.AgentConn == nil {
114+
return nil, xerrors.New("agentconn config must be set")
115+
}
116+
return agentconn.NewRunner(client, *t.AgentConn), nil
108117
case LoadTestTypePlacebo:
109118
if t.Placebo == nil {
110119
return nil, xerrors.New("placebo config must be set")
111120
}
112-
113121
return placebo.NewRunner(*t.Placebo), nil
114122
case LoadTestTypeWorkspaceBuild:
115123
if t.WorkspaceBuild == nil {
116124
return nil, xerrors.Errorf("workspacebuild config must be set")
117125
}
118-
119126
return workspacebuild.NewRunner(client, *t.WorkspaceBuild), nil
120127
default:
121128
return nil, xerrors.Errorf("unknown test type %q", t.Type)
@@ -155,6 +162,15 @@ func (s *LoadTestStrategy) Validate() error {
155162

156163
func (t *LoadTest) Validate() error {
157164
switch t.Type {
165+
case LoadTestTypeAgentConn:
166+
if t.AgentConn == nil {
167+
return xerrors.Errorf("agentconn test type must specify agentconn")
168+
}
169+
170+
err := t.AgentConn.Validate()
171+
if err != nil {
172+
return xerrors.Errorf("validate agentconn: %w", err)
173+
}
158174
case LoadTestTypePlacebo:
159175
if t.Placebo == nil {
160176
return xerrors.Errorf("placebo test type must specify placebo")

loadtest/agentconn/config.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package agentconn
2+
3+
import (
4+
"net/url"
5+
6+
"github.com/google/uuid"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/coderd/httpapi"
10+
)
11+
12+
type ConnectionMode string
13+
14+
const (
15+
ConnectionModeDirect ConnectionMode = "direct"
16+
ConnectionModeDerp ConnectionMode = "derp"
17+
)
18+
19+
type Config struct {
20+
// AgentID is the ID of the agent to connect to.
21+
AgentID uuid.UUID `json:"agent_id"`
22+
// ConnectionMode is the strategy to use when connecting to the agent.
23+
ConnectionMode ConnectionMode `json:"connection_mode"`
24+
// HoldDuration is the duration to hold the connection open for. If set to
25+
// 0, the connection will be closed immediately after making each request
26+
// once.
27+
HoldDuration httpapi.Duration `json:"hold_duration"`
28+
29+
// Connections is the list of connections to make to services running
30+
// inside the workspace. Only HTTP connections are supported.
31+
Connections []Connection `json:"connections"`
32+
}
33+
34+
type Connection struct {
35+
// URL is the address to connect to (e.g. "http://127.0.0.1:8080/path"). The
36+
// endpoint must respond with a any response within timeout. The IP address
37+
// is ignored and the connection is made to the agent's WireGuard IP
38+
// instead.
39+
URL string `json:"url"`
40+
// Interval is the duration to wait between connections to this endpoint. If
41+
// set to 0, the connection will only be made once. Must be set to 0 if
42+
// the parent config's hold_duration is set to 0.
43+
Interval httpapi.Duration `json:"interval"`
44+
// Timeout is the duration to wait for a connection to this endpoint to
45+
// succeed. If set to 0, the default timeout will be used.
46+
Timeout httpapi.Duration `json:"timeout"`
47+
}
48+
49+
func (c Config) Validate() error {
50+
if c.AgentID == uuid.Nil {
51+
return xerrors.New("agent_id must be set")
52+
}
53+
if c.ConnectionMode == "" {
54+
return xerrors.New("connection_mode must be set")
55+
}
56+
switch c.ConnectionMode {
57+
case ConnectionModeDirect:
58+
case ConnectionModeDerp:
59+
default:
60+
return xerrors.Errorf("invalid connection_mode: %q", c.ConnectionMode)
61+
}
62+
if c.HoldDuration < 0 {
63+
return xerrors.New("hold_duration must be a positive value")
64+
}
65+
66+
for i, conn := range c.Connections {
67+
if conn.URL == "" {
68+
return xerrors.Errorf("connections[%d].url must be set", i)
69+
}
70+
u, err := url.Parse(conn.URL)
71+
if err != nil {
72+
return xerrors.Errorf("connections[%d].url is not a valid URL: %w", i, err)
73+
}
74+
if u.Scheme != "http" {
75+
return xerrors.Errorf("connections[%d].url has an unsupported scheme %q, only http is supported", i, u.Scheme)
76+
}
77+
if conn.Interval < 0 {
78+
return xerrors.Errorf("connections[%d].interval must be a positive value", i)
79+
}
80+
if conn.Interval > 0 && c.HoldDuration == 0 {
81+
return xerrors.Errorf("connections[%d].interval must be 0 if hold_duration is 0", i)
82+
}
83+
if conn.Timeout < 0 {
84+
return xerrors.Errorf("connections[%d].timeout must be a positive value", i)
85+
}
86+
}
87+
88+
return nil
89+
}

loadtest/agentconn/config_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package agentconn_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/google/uuid"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/coderd/httpapi"
11+
"github.com/coder/coder/loadtest/agentconn"
12+
)
13+
14+
func Test_Config(t *testing.T) {
15+
t.Parallel()
16+
17+
id := uuid.New()
18+
cases := []struct {
19+
name string
20+
config agentconn.Config
21+
errContains string
22+
}{
23+
{
24+
name: "OK",
25+
config: agentconn.Config{
26+
AgentID: id,
27+
ConnectionMode: agentconn.ConnectionModeDirect,
28+
HoldDuration: httpapi.Duration(time.Minute),
29+
Connections: []agentconn.Connection{
30+
{
31+
URL: "http://localhost:8080/path",
32+
Interval: httpapi.Duration(time.Second),
33+
Timeout: httpapi.Duration(time.Second),
34+
},
35+
{
36+
URL: "http://localhost:8000/differentpath",
37+
Interval: httpapi.Duration(2 * time.Second),
38+
Timeout: httpapi.Duration(2 * time.Second),
39+
},
40+
},
41+
},
42+
},
43+
{
44+
name: "NoAgentID",
45+
config: agentconn.Config{
46+
AgentID: uuid.Nil,
47+
ConnectionMode: agentconn.ConnectionModeDirect,
48+
HoldDuration: 0,
49+
Connections: nil,
50+
},
51+
errContains: "agent_id must be set",
52+
},
53+
{
54+
name: "NoConnectionMode",
55+
config: agentconn.Config{
56+
AgentID: id,
57+
ConnectionMode: "",
58+
HoldDuration: 0,
59+
Connections: nil,
60+
},
61+
errContains: "connection_mode must be set",
62+
},
63+
{
64+
name: "InvalidConnectionMode",
65+
config: agentconn.Config{
66+
AgentID: id,
67+
ConnectionMode: "blah",
68+
HoldDuration: 0,
69+
Connections: nil,
70+
},
71+
errContains: "invalid connection_mode",
72+
},
73+
{
74+
name: "NegativeHoldDuration",
75+
config: agentconn.Config{
76+
AgentID: id,
77+
ConnectionMode: agentconn.ConnectionModeDerp,
78+
HoldDuration: -1,
79+
Connections: nil,
80+
},
81+
errContains: "hold_duration must be a positive value",
82+
},
83+
{
84+
name: "ConnectionNoURL",
85+
config: agentconn.Config{
86+
AgentID: id,
87+
ConnectionMode: agentconn.ConnectionModeDirect,
88+
HoldDuration: 1,
89+
Connections: []agentconn.Connection{{
90+
URL: "",
91+
Interval: 0,
92+
Timeout: 0,
93+
}},
94+
},
95+
errContains: "connections[0].url must be set",
96+
},
97+
{
98+
name: "ConnectionInvalidURL",
99+
config: agentconn.Config{
100+
AgentID: id,
101+
ConnectionMode: agentconn.ConnectionModeDirect,
102+
HoldDuration: 1,
103+
Connections: []agentconn.Connection{{
104+
URL: string([]byte{0x7f}),
105+
Interval: 0,
106+
Timeout: 0,
107+
}},
108+
},
109+
errContains: "connections[0].url is not a valid URL",
110+
},
111+
{
112+
name: "ConnectionInvalidURLScheme",
113+
config: agentconn.Config{
114+
AgentID: id,
115+
ConnectionMode: agentconn.ConnectionModeDirect,
116+
HoldDuration: 1,
117+
Connections: []agentconn.Connection{{
118+
URL: "blah://localhost:8080",
119+
Interval: 0,
120+
Timeout: 0,
121+
}},
122+
},
123+
errContains: "connections[0].url has an unsupported scheme",
124+
},
125+
{
126+
name: "ConnectionNegativeInterval",
127+
config: agentconn.Config{
128+
AgentID: id,
129+
ConnectionMode: agentconn.ConnectionModeDirect,
130+
HoldDuration: 1,
131+
Connections: []agentconn.Connection{{
132+
URL: "http://localhost:8080",
133+
Interval: -1,
134+
Timeout: 0,
135+
}},
136+
},
137+
errContains: "connections[0].interval must be a positive value",
138+
},
139+
{
140+
name: "ConnectionIntervalMustBeZero",
141+
config: agentconn.Config{
142+
AgentID: id,
143+
ConnectionMode: agentconn.ConnectionModeDirect,
144+
HoldDuration: 0,
145+
Connections: []agentconn.Connection{{
146+
URL: "http://localhost:8080",
147+
Interval: 1,
148+
Timeout: 0,
149+
}},
150+
},
151+
errContains: "connections[0].interval must be 0 if hold_duration is 0",
152+
},
153+
{
154+
name: "ConnectionNegativeTimeout",
155+
config: agentconn.Config{
156+
AgentID: id,
157+
ConnectionMode: agentconn.ConnectionModeDirect,
158+
HoldDuration: 1,
159+
Connections: []agentconn.Connection{{
160+
URL: "http://localhost:8080",
161+
Interval: 0,
162+
Timeout: -1,
163+
}},
164+
},
165+
errContains: "connections[0].timeout must be a positive value",
166+
},
167+
}
168+
169+
for _, c := range cases {
170+
c := c
171+
172+
t.Run(c.name, func(t *testing.T) {
173+
t.Parallel()
174+
175+
err := c.config.Validate()
176+
if c.errContains != "" {
177+
require.Error(t, err)
178+
require.Contains(t, err.Error(), c.errContains)
179+
} else {
180+
require.NoError(t, err)
181+
}
182+
})
183+
}
184+
}

0 commit comments

Comments
 (0)