- Notifications
You must be signed in to change notification settings - Fork1k
feat: add scaletest Runner for dynamicparameters load gen#19890
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes from1 commit
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package dynamicparameters | ||
import"github.com/google/uuid" | ||
typeConfigstruct { | ||
TemplateVersion uuid.UUID`json:"template_version"` | ||
SessionTokenstring`json:"session_token"` | ||
Metrics*Metrics`json:"-"` | ||
MetricLabelValues []string`json:"metric_label_values"` | ||
} | ||
Comment on lines +5 to +10 Member
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package dynamicparameters | ||
import "github.com/prometheus/client_golang/prometheus" | ||
type Metrics struct { | ||
LatencyInitialResponseSeconds prometheus.HistogramVec | ||
LatencyChangeResponseSeconds prometheus.HistogramVec | ||
} | ||
func NewMetrics(reg prometheus.Registerer, labelNames ...string) *Metrics { | ||
m := &Metrics{ | ||
LatencyInitialResponseSeconds: *prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Namespace: "coderd", | ||
Subsystem: "scaletest", | ||
Name: "dynamic_parameters_latency_initial_response_seconds", | ||
Help: "Time in seconds to get the initial dynamic parameters response from start of request.", | ||
}, labelNames), | ||
LatencyChangeResponseSeconds: *prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Namespace: "coderd", | ||
Subsystem: "scaletest", | ||
Name: "dynamic_parameters_latency_change_response_seconds", | ||
Help: "Time in seconds to between sending a dynamic parameters change request and receiving the response.", | ||
}, labelNames), | ||
} | ||
reg.MustRegister(m.LatencyInitialResponseSeconds) | ||
reg.MustRegister(m.LatencyChangeResponseSeconds) | ||
return m | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,114 @@ | ||||
package dynamicparameters | ||||
import ( | ||||
"context" | ||||
"fmt" | ||||
"io" | ||||
"slices" | ||||
"time" | ||||
"golang.org/x/xerrors" | ||||
"github.com/coder/coder/v2/codersdk" | ||||
"github.com/coder/coder/v2/scaletest/harness" | ||||
"github.com/coder/websocket" | ||||
) | ||||
type Runner struct { | ||||
client *codersdk.Client | ||||
cfg Config | ||||
} | ||||
var _ harness.Runnable = &Runner{} | ||||
func NewRunner(client *codersdk.Client, cfg Config) *Runner { | ||||
clone := codersdk.New(client.URL) | ||||
clone.HTTPClient = client.HTTPClient | ||||
clone.SetLogger(client.Logger()) | ||||
clone.SetSessionToken(cfg.SessionToken) | ||||
return &Runner{ | ||||
client: clone, | ||||
cfg: cfg, | ||||
} | ||||
} | ||||
// Run executes the dynamic parameters test, which: | ||||
// | ||||
// 1. connects to the dynamic parameters stream | ||||
// 2. waits for the initial response | ||||
// 3. sends a change request | ||||
// 4. waits for the change response | ||||
// 5. closes the stream | ||||
func (r *Runner) Run(ctx context.Context, _ string, logs io.Writer) (retErr error) { | ||||
startTime := time.Now() | ||||
stream, err := r.client.TemplateVersionDynamicParameters(ctx, codersdk.Me, r.cfg.TemplateVersion) | ||||
if err != nil { | ||||
return xerrors.Errorf("connect to dynamic parameters stream: %w", err) | ||||
} | ||||
defer stream.Close(websocket.StatusNormalClosure) | ||||
respCh := stream.Chan() | ||||
var initTime time.Time | ||||
select { | ||||
case <-ctx.Done(): | ||||
return ctx.Err() | ||||
case resp, ok := <-respCh: | ||||
if !ok { | ||||
return xerrors.Errorf("dynamic parameters stream closed before initial response") | ||||
} | ||||
initTime = time.Now() | ||||
r.cfg.Metrics.LatencyInitialResponseSeconds. | ||||
WithLabelValues(r.cfg.MetricLabelValues...). | ||||
Observe(initTime.Sub(startTime).Seconds()) | ||||
_, _ = fmt.Fprintf(logs, "initial response: %+v\n", resp) | ||||
if !slices.ContainsFunc(resp.Parameters, func(p codersdk.PreviewParameter) bool { | ||||
return p.Name == "zero" | ||||
}) { | ||||
return xerrors.Errorf("mising expected parameter: 'zero'") | ||||
spikecurtis marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||||
} | ||||
if err := checkNoDiagnostics(resp); err != nil { | ||||
return xerrors.Errorf("unexpected initial response diagnostics: %w", err) | ||||
} | ||||
} | ||||
err = stream.Send(codersdk.DynamicParametersRequest{ | ||||
ID: 1, | ||||
Inputs: map[string]string{ | ||||
"zero": "B", | ||||
}, | ||||
}) | ||||
if err != nil { | ||||
return xerrors.Errorf("send change request: %w", err) | ||||
} | ||||
select { | ||||
case <-ctx.Done(): | ||||
return ctx.Err() | ||||
case resp, ok := <-respCh: | ||||
if !ok { | ||||
return xerrors.Errorf("dynamic parameters stream closed before change response") | ||||
} | ||||
_, _ = fmt.Fprintf(logs, "change response: %+v\n", resp) | ||||
r.cfg.Metrics.LatencyChangeResponseSeconds. | ||||
WithLabelValues(r.cfg.MetricLabelValues...). | ||||
Observe(time.Since(initTime).Seconds()) | ||||
if resp.ID != 1 { | ||||
return xerrors.Errorf("unexpected response ID: %d", resp.ID) | ||||
} | ||||
if err := checkNoDiagnostics(resp); err != nil { | ||||
return xerrors.Errorf("unexpected change response diagnostics: %w", err) | ||||
} | ||||
return nil | ||||
Comment on lines +86 to +100 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. We do not assert any param condition on the response. I wonder if we can refactor the
It really streamlines the assertion code to something like: coderdtest.AssertParameter(t,"groups",resp.Parameters).Exists().Options(database.EveryoneGroup,"admin","auditor").Value("admin") It just currently only works in unit tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I feel like this is missing the point --- we are scale testing, not functional testing. I'm not looking to check that the response we get is "correct" in any detailed sense of the term, just that the response has not thrown errors and is doing the computation we want it to. | ||||
} | ||||
} | ||||
func checkNoDiagnostics(resp codersdk.DynamicParametersResponse) error { | ||||
if len(resp.Diagnostics) != 0 { | ||||
return xerrors.Errorf("unexpected response diagnostics: %v", resp.Diagnostics) | ||||
} | ||||
for _, param := range resp.Parameters { | ||||
if len(param.Diagnostics) != 0 { | ||||
return xerrors.Errorf("unexpected parameter diagnostics for '%s': %v", param.Name, param.Diagnostics) | ||||
} | ||||
} | ||||
return nil | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package dynamicparameters_test | ||
import ( | ||
"strings" | ||
"testing" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/stretchr/testify/require" | ||
"github.com/coder/coder/v2/coderd/coderdtest" | ||
"github.com/coder/coder/v2/scaletest/dynamicparameters" | ||
"github.com/coder/coder/v2/testutil" | ||
) | ||
func TestRun(t *testing.T) { | ||
t.Parallel() | ||
ctx := testutil.Context(t, testutil.WaitLong) | ||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) | ||
first := coderdtest.CreateFirstUser(t, client) | ||
userClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) | ||
orgID := first.OrganizationID | ||
dynamicParametersTerraformSource, err := dynamicparameters.TemplateContent() | ||
require.NoError(t, err) | ||
template, version := coderdtest.DynamicParameterTemplate(t, client, orgID, coderdtest.DynamicParameterTemplateParams{ | ||
MainTF: dynamicParametersTerraformSource, | ||
Plan: nil, | ||
ModulesArchive: nil, | ||
StaticParams: nil, | ||
}) | ||
Emyrk marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
reg := prometheus.NewRegistry() | ||
cfg := dynamicparameters.Config{ | ||
TemplateVersion: version.ID, | ||
SessionToken: userClient.SessionToken(), | ||
Metrics: dynamicparameters.NewMetrics(reg, "template", "test_label_name"), | ||
MetricLabelValues: []string{template.Name, "test_label_value"}, | ||
} | ||
runner := dynamicparameters.NewRunner(userClient, cfg) | ||
var logs strings.Builder | ||
err = runner.Run(ctx, t.Name(), &logs) | ||
t.Log("Runner logs:\n\n" + logs.String()) | ||
require.NoError(t, err) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package dynamicparameters | ||
import ( | ||
_ "embed" | ||
"strings" | ||
"text/template" | ||
"github.com/coder/coder/v2/cryptorand" | ||
) | ||
//go:embed workspace-template.tf | ||
var templateContent string | ||
func TemplateContent() (string, error) { | ||
randomString, err := cryptorand.String(8) | ||
if err != nil { | ||
return "", err | ||
} | ||
// Parse the template | ||
tmpl, err := template.New("workspace-template").Parse(templateContent) | ||
if err != nil { | ||
return "", err | ||
} | ||
// Execute the template with the random string | ||
var result strings.Builder | ||
err = tmpl.Execute(&result, map[string]string{ | ||
"RandomString": randomString, | ||
}) | ||
if err != nil { | ||
// Return the original template if execution fails | ||
return "", err | ||
spikecurtis marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
} | ||
return result.String(), nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# Cache busting string so each copy of the template is unique: {{.RandomString}} | ||
terraform { | ||
required_providers { | ||
coder = { | ||
source = "coder/coder" | ||
version = "2.5.3" | ||
} | ||
} | ||
} | ||
locals { | ||
one_options = { | ||
"A" = ["AA", "AB"] | ||
"B" = ["BA", "BB"] | ||
} | ||
three_options = { | ||
"AA" = ["AAA", "AAB"] | ||
"AB" = ["ABA", "ABB"] | ||
"BA" = ["BAA", "BAB"] | ||
"BB" = ["BBA", "BBB"] | ||
} | ||
username = data.coder_workspace_owner.me.name | ||
} | ||
data "coder_workspace_owner" "me" {} | ||
data "coder_parameter" "zero" { | ||
name = "zero" | ||
display_name = "Root" | ||
description = "Hello ${local.username}, pick your next parameter using this `dropdown` parameter." | ||
form_type = "dropdown" | ||
mutable = true | ||
default = "A" | ||
option { | ||
value = "A" | ||
name = "A" | ||
} | ||
option { | ||
value = "B" | ||
name = "B" | ||
} | ||
} | ||
data "coder_parameter" "one" { | ||
name = "One" | ||
display_name = "Level One" | ||
description = "This is the first level." | ||
type = "list(string)" | ||
form_type = "multi-select" | ||
order = 2 | ||
mutable = true | ||
default = "[\"${local.one_options[data.coder_parameter.zero.value][0]}\"]" | ||
dynamic "option" { | ||
for_each = local.one_options[data.coder_parameter.zero.value] | ||
content { | ||
name = option.value | ||
value = option.value | ||
} | ||
} | ||
} | ||
data "coder_parameter" "two" { | ||
name = "Two" | ||
display_name = "Level Two" | ||
description = "This is the second level." | ||
type = "string" | ||
form_type = "textarea" | ||
order = 3 | ||
mutable = true | ||
default = trim(data.coder_parameter.one.value, "[\"]") | ||
} | ||
data "coder_parameter" "three" { | ||
name = "Three" | ||
display_name = "Level Three" | ||
description = "This is the first level." | ||
spikecurtis marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
type = "string" | ||
form_type = "radio" | ||
order = 4 | ||
mutable = true | ||
default = local.three_options[data.coder_parameter.two.value][0] | ||
dynamic "option" { | ||
for_each = local.three_options[data.coder_parameter.two.value] | ||
content { | ||
name = option.value | ||
value = option.value | ||
} | ||
} | ||
} | ||
data "coder_parameter" "four" { | ||
name = "four" | ||
display_name = "Level Four" | ||
description = "This is the last level." | ||
order = 5 | ||
type = "string" | ||
form_type = "radio" | ||
default = "a_fake_value_to_satify_import" | ||
spikecurtis marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
option { | ||
name = format("%s-%s", local.username, data.coder_parameter.three.value) | ||
value = "a_fake_value_to_satify_import" | ||
spikecurtis marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
} | ||
dynamic "option" { | ||
for_each = data.coder_workspace_owner.me.rbac_roles | ||
content { | ||
name = format("%s-%s", option.value.name, data.coder_parameter.three.value) | ||
value = option.value.name | ||
} | ||
} | ||
} |