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

feat: add new loadtest type agentconn #4899

Merged
merged 2 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 18 additions & 2 deletions cli/loadtestconfig.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
}
185 changes: 185 additions & 0 deletions loadtest/agentconn/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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.Must(uuid.NewRandom())
deansheather marked this conversation as resolved.
Show resolved Hide resolved

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)
}
})
}
}