Skip to content

Commit

Permalink
feat: add new loadtest type agentconn (#4899)
Browse files Browse the repository at this point in the history
  • Loading branch information
deansheather committed Nov 7, 2022
1 parent 56b963a commit f918977
Show file tree
Hide file tree
Showing 5 changed files with 898 additions and 2 deletions.
20 changes: 18 additions & 2 deletions cli/loadtestconfig.go
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/loadtest/agentconn"
"github.com/coder/coder/loadtest/harness"
"github.com/coder/coder/loadtest/placebo"
"github.com/coder/coder/loadtest/workspacebuild"
Expand Down Expand Up @@ -86,6 +87,7 @@ func (s LoadTestStrategy) ExecutionStrategy() harness.ExecutionStrategy {
type LoadTestType string

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

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

func (t LoadTest) NewRunner(client *codersdk.Client) (harness.Runnable, error) {
switch t.Type {
case LoadTestTypeAgentConn:
if t.AgentConn == nil {
return nil, xerrors.New("agentconn config must be set")
}
return agentconn.NewRunner(client, *t.AgentConn), nil
case LoadTestTypePlacebo:
if t.Placebo == nil {
return nil, xerrors.New("placebo config must be set")
}

return placebo.NewRunner(*t.Placebo), nil
case LoadTestTypeWorkspaceBuild:
if t.WorkspaceBuild == nil {
return nil, xerrors.Errorf("workspacebuild config must be set")
}

return workspacebuild.NewRunner(client, *t.WorkspaceBuild), nil
default:
return nil, xerrors.Errorf("unknown test type %q", t.Type)
Expand Down Expand Up @@ -155,6 +162,15 @@ func (s *LoadTestStrategy) Validate() error {

func (t *LoadTest) Validate() error {
switch t.Type {
case LoadTestTypeAgentConn:
if t.AgentConn == nil {
return xerrors.Errorf("agentconn test type must specify agentconn")
}

err := t.AgentConn.Validate()
if err != nil {
return xerrors.Errorf("validate agentconn: %w", err)
}
case LoadTestTypePlacebo:
if t.Placebo == nil {
return xerrors.Errorf("placebo test type must specify placebo")
Expand Down
89 changes: 89 additions & 0 deletions loadtest/agentconn/config.go
@@ -0,0 +1,89 @@
package agentconn

import (
"net/url"

"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/httpapi"
)

type ConnectionMode string

const (
ConnectionModeDirect ConnectionMode = "direct"
ConnectionModeDerp ConnectionMode = "derp"
)

type Config struct {
// AgentID is the ID of the agent to connect to.
AgentID uuid.UUID `json:"agent_id"`
// ConnectionMode is the strategy to use when connecting to the agent.
ConnectionMode ConnectionMode `json:"connection_mode"`
// HoldDuration is the duration to hold the connection open for. If set to
// 0, the connection will be closed immediately after making each request
// once.
HoldDuration httpapi.Duration `json:"hold_duration"`

// Connections is the list of connections to make to services running
// inside the workspace. Only HTTP connections are supported.
Connections []Connection `json:"connections"`
}

type Connection struct {
// URL is the address to connect to (e.g. "http://127.0.0.1:8080/path"). The
// endpoint must respond with a any response within timeout. The IP address
// is ignored and the connection is made to the agent's WireGuard IP
// instead.
URL string `json:"url"`
// Interval is the duration to wait between connections to this endpoint. If
// set to 0, the connection will only be made once. Must be set to 0 if
// the parent config's hold_duration is set to 0.
Interval httpapi.Duration `json:"interval"`
// Timeout is the duration to wait for a connection to this endpoint to
// succeed. If set to 0, the default timeout will be used.
Timeout httpapi.Duration `json:"timeout"`
}

func (c Config) Validate() error {
if c.AgentID == uuid.Nil {
return xerrors.New("agent_id must be set")
}
if c.ConnectionMode == "" {
return xerrors.New("connection_mode must be set")
}
switch c.ConnectionMode {
case ConnectionModeDirect:
case ConnectionModeDerp:
default:
return xerrors.Errorf("invalid connection_mode: %q", c.ConnectionMode)
}
if c.HoldDuration < 0 {
return xerrors.New("hold_duration must be a positive value")
}

for i, conn := range c.Connections {
if conn.URL == "" {
return xerrors.Errorf("connections[%d].url must be set", i)
}
u, err := url.Parse(conn.URL)
if err != nil {
return xerrors.Errorf("connections[%d].url is not a valid URL: %w", i, err)
}
if u.Scheme != "http" {
return xerrors.Errorf("connections[%d].url has an unsupported scheme %q, only http is supported", i, u.Scheme)
}
if conn.Interval < 0 {
return xerrors.Errorf("connections[%d].interval must be a positive value", i)
}
if conn.Interval > 0 && c.HoldDuration == 0 {
return xerrors.Errorf("connections[%d].interval must be 0 if hold_duration is 0", i)
}
if conn.Timeout < 0 {
return xerrors.Errorf("connections[%d].timeout must be a positive value", i)
}
}

return nil
}
184 changes: 184 additions & 0 deletions loadtest/agentconn/config_test.go
@@ -0,0 +1,184 @@
package agentconn_test

import (
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/loadtest/agentconn"
)

func Test_Config(t *testing.T) {
t.Parallel()

id := uuid.New()
cases := []struct {
name string
config agentconn.Config
errContains string
}{
{
name: "OK",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: httpapi.Duration(time.Minute),
Connections: []agentconn.Connection{
{
URL: "http://localhost:8080/path",
Interval: httpapi.Duration(time.Second),
Timeout: httpapi.Duration(time.Second),
},
{
URL: "http://localhost:8000/differentpath",
Interval: httpapi.Duration(2 * time.Second),
Timeout: httpapi.Duration(2 * time.Second),
},
},
},
},
{
name: "NoAgentID",
config: agentconn.Config{
AgentID: uuid.Nil,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 0,
Connections: nil,
},
errContains: "agent_id must be set",
},
{
name: "NoConnectionMode",
config: agentconn.Config{
AgentID: id,
ConnectionMode: "",
HoldDuration: 0,
Connections: nil,
},
errContains: "connection_mode must be set",
},
{
name: "InvalidConnectionMode",
config: agentconn.Config{
AgentID: id,
ConnectionMode: "blah",
HoldDuration: 0,
Connections: nil,
},
errContains: "invalid connection_mode",
},
{
name: "NegativeHoldDuration",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDerp,
HoldDuration: -1,
Connections: nil,
},
errContains: "hold_duration must be a positive value",
},
{
name: "ConnectionNoURL",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 1,
Connections: []agentconn.Connection{{
URL: "",
Interval: 0,
Timeout: 0,
}},
},
errContains: "connections[0].url must be set",
},
{
name: "ConnectionInvalidURL",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 1,
Connections: []agentconn.Connection{{
URL: string([]byte{0x7f}),
Interval: 0,
Timeout: 0,
}},
},
errContains: "connections[0].url is not a valid URL",
},
{
name: "ConnectionInvalidURLScheme",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 1,
Connections: []agentconn.Connection{{
URL: "blah://localhost:8080",
Interval: 0,
Timeout: 0,
}},
},
errContains: "connections[0].url has an unsupported scheme",
},
{
name: "ConnectionNegativeInterval",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 1,
Connections: []agentconn.Connection{{
URL: "http://localhost:8080",
Interval: -1,
Timeout: 0,
}},
},
errContains: "connections[0].interval must be a positive value",
},
{
name: "ConnectionIntervalMustBeZero",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 0,
Connections: []agentconn.Connection{{
URL: "http://localhost:8080",
Interval: 1,
Timeout: 0,
}},
},
errContains: "connections[0].interval must be 0 if hold_duration is 0",
},
{
name: "ConnectionNegativeTimeout",
config: agentconn.Config{
AgentID: id,
ConnectionMode: agentconn.ConnectionModeDirect,
HoldDuration: 1,
Connections: []agentconn.Connection{{
URL: "http://localhost:8080",
Interval: 0,
Timeout: -1,
}},
},
errContains: "connections[0].timeout must be a positive value",
},
}

for _, c := range cases {
c := c

t.Run(c.name, func(t *testing.T) {
t.Parallel()

err := c.config.Validate()
if c.errContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.errContains)
} else {
require.NoError(t, err)
}
})
}
}

0 comments on commit f918977

Please sign in to comment.