Skip to content

Commit cd0ea6e

Browse files
committed
Expose browser pool warmup configuration
Read start URL, timeout, and fill rate into durable data-source state. Validate SDK response types and ranges while preserving omitted values and known zero.
1 parent a33501f commit cd0ea6e

3 files changed

Lines changed: 162 additions & 41 deletions

File tree

docs/data-sources/browser_pool.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ Lookup durable Kernel browser pool configuration.
2424
### Read-Only
2525

2626
- `extension_ids` (List of String) Resolved extension IDs attached to the pool, in load order.
27+
- `fill_rate_per_minute` (Number) Percentage of the pool filled per minute.
2728
- `headless` (Boolean) Whether browsers use a headless image.
2829
- `kiosk_mode` (Boolean) Whether browsers launch in kiosk mode.
2930
- `profile_id` (String) Resolved profile ID attached to the pool, if any.
3031
- `proxy_id` (String) Proxy ID attached to browsers in the pool, if any.
3132
- `size` (Number) Number of browsers maintained in the pool.
33+
- `start_url` (String) URL opened when a browser is warmed into the pool, if configured.
3234
- `stealth` (Boolean) Whether browsers launch in stealth mode.
35+
- `timeout_seconds` (Number) Default idle timeout in seconds for acquired browsers.

internal/datasources/browserpool/datasource.go

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ var (
2424
_ datasource.DataSourceWithConfigure = (*browserPoolDataSource)(nil)
2525
)
2626

27+
const (
28+
minBrowserPoolTimeoutSeconds = 10
29+
maxBrowserPoolTimeoutSeconds = 259200
30+
minBrowserPoolFillRate = 0
31+
)
32+
2733
type browserPoolClient interface {
2834
DefaultProjectID() string
2935
GetBrowserPool(context.Context, string, string) (*kernel.BrowserPool, error)
@@ -34,16 +40,19 @@ type browserPoolDataSource struct {
3440
}
3541

3642
type browserPoolModel struct {
37-
ID types.String `tfsdk:"id"`
38-
Name types.String `tfsdk:"name"`
39-
ProjectID types.String `tfsdk:"project_id"`
40-
Size types.Int64 `tfsdk:"size"`
41-
ProfileID types.String `tfsdk:"profile_id"`
42-
ExtensionIDs types.List `tfsdk:"extension_ids"`
43-
ProxyID types.String `tfsdk:"proxy_id"`
44-
Headless types.Bool `tfsdk:"headless"`
45-
KioskMode types.Bool `tfsdk:"kiosk_mode"`
46-
Stealth types.Bool `tfsdk:"stealth"`
43+
ID types.String `tfsdk:"id"`
44+
Name types.String `tfsdk:"name"`
45+
ProjectID types.String `tfsdk:"project_id"`
46+
Size types.Int64 `tfsdk:"size"`
47+
ProfileID types.String `tfsdk:"profile_id"`
48+
ExtensionIDs types.List `tfsdk:"extension_ids"`
49+
ProxyID types.String `tfsdk:"proxy_id"`
50+
Headless types.Bool `tfsdk:"headless"`
51+
KioskMode types.Bool `tfsdk:"kiosk_mode"`
52+
Stealth types.Bool `tfsdk:"stealth"`
53+
StartURL types.String `tfsdk:"start_url"`
54+
TimeoutSeconds types.Int64 `tfsdk:"timeout_seconds"`
55+
FillRatePerMinute types.Int64 `tfsdk:"fill_rate_per_minute"`
4756
}
4857

4958
func NewDataSource() datasource.DataSource {
@@ -108,6 +117,18 @@ func (d *browserPoolDataSource) Schema(_ context.Context, _ datasource.SchemaReq
108117
Computed: true,
109118
MarkdownDescription: "Whether browsers launch in stealth mode.",
110119
},
120+
"start_url": dschema.StringAttribute{
121+
Computed: true,
122+
MarkdownDescription: "URL opened when a browser is warmed into the pool, if configured.",
123+
},
124+
"timeout_seconds": dschema.Int64Attribute{
125+
Computed: true,
126+
MarkdownDescription: "Default idle timeout in seconds for acquired browsers.",
127+
},
128+
"fill_rate_per_minute": dschema.Int64Attribute{
129+
Computed: true,
130+
MarkdownDescription: "Percentage of the pool filled per minute.",
131+
},
111132
},
112133
}
113134
}
@@ -236,15 +257,18 @@ func flattenBrowserPool(pool kernel.BrowserPool) (browserPoolModel, diag.Diagnos
236257

237258
config := pool.BrowserPoolConfig
238259
return browserPoolModel{
239-
ID: types.StringValue(pool.ID),
240-
Name: name,
241-
Size: types.Int64Value(config.Size),
242-
ProfileID: flattenResolvedProfileID(pool, &diags),
243-
ExtensionIDs: flattenResolvedExtensionIDs(pool, &diags),
244-
ProxyID: flattenOptionalString("browser_pool_config.proxy_id", config.JSON.ProxyID.Raw(), config.JSON.ProxyID.Valid(), config.ProxyID, &diags),
245-
Headless: flattenOptionalBool("browser_pool_config.headless", config.JSON.Headless.Raw(), config.JSON.Headless.Valid(), config.Headless, &diags),
246-
KioskMode: flattenOptionalBool("browser_pool_config.kiosk_mode", config.JSON.KioskMode.Raw(), config.JSON.KioskMode.Valid(), config.KioskMode, &diags),
247-
Stealth: flattenOptionalBool("browser_pool_config.stealth", config.JSON.Stealth.Raw(), config.JSON.Stealth.Valid(), config.Stealth, &diags),
260+
ID: types.StringValue(pool.ID),
261+
Name: name,
262+
Size: types.Int64Value(config.Size),
263+
ProfileID: flattenResolvedProfileID(pool, &diags),
264+
ExtensionIDs: flattenResolvedExtensionIDs(pool, &diags),
265+
ProxyID: flattenOptionalString("browser_pool_config.proxy_id", config.JSON.ProxyID.Raw(), config.JSON.ProxyID.Valid(), config.ProxyID, &diags),
266+
Headless: flattenOptionalBool("browser_pool_config.headless", config.JSON.Headless.Raw(), config.JSON.Headless.Valid(), config.Headless, &diags),
267+
KioskMode: flattenOptionalBool("browser_pool_config.kiosk_mode", config.JSON.KioskMode.Raw(), config.JSON.KioskMode.Valid(), config.KioskMode, &diags),
268+
Stealth: flattenOptionalBool("browser_pool_config.stealth", config.JSON.Stealth.Raw(), config.JSON.Stealth.Valid(), config.Stealth, &diags),
269+
StartURL: flattenOptionalString("browser_pool_config.start_url", config.JSON.StartURL.Raw(), config.JSON.StartURL.Valid(), config.StartURL, &diags),
270+
TimeoutSeconds: flattenTimeoutSeconds(config.JSON.TimeoutSeconds.Raw(), config.JSON.TimeoutSeconds.Valid(), config.TimeoutSeconds, &diags),
271+
FillRatePerMinute: flattenFillRatePerMinute(config.JSON.FillRatePerMinute.Raw(), config.JSON.FillRatePerMinute.Valid(), config.FillRatePerMinute, &diags),
248272
}, diags
249273
}
250274

@@ -270,6 +294,41 @@ func flattenOptionalBool(field, raw string, valid bool, value bool, diags *diag.
270294
return types.BoolValue(value)
271295
}
272296

297+
func flattenTimeoutSeconds(raw string, valid bool, value int64, diags *diag.Diagnostics) types.Int64 {
298+
result := flattenOptionalInt64("browser_pool_config.timeout_seconds", raw, valid, value, diags)
299+
if result.IsNull() {
300+
return result
301+
}
302+
if value < minBrowserPoolTimeoutSeconds || value > maxBrowserPoolTimeoutSeconds {
303+
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.timeout_seconds")
304+
return types.Int64Null()
305+
}
306+
return result
307+
}
308+
309+
func flattenFillRatePerMinute(raw string, valid bool, value int64, diags *diag.Diagnostics) types.Int64 {
310+
result := flattenOptionalInt64("browser_pool_config.fill_rate_per_minute", raw, valid, value, diags)
311+
if result.IsNull() {
312+
return result
313+
}
314+
if value < minBrowserPoolFillRate {
315+
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.fill_rate_per_minute")
316+
return types.Int64Null()
317+
}
318+
return result
319+
}
320+
321+
func flattenOptionalInt64(field, raw string, valid bool, value int64, diags *diag.Diagnostics) types.Int64 {
322+
if raw == "" {
323+
return types.Int64Null()
324+
}
325+
if !validResponseInt64(raw, valid, value) {
326+
datasources.AddInvalidResponseField(diags, "Browser Pool", field)
327+
return types.Int64Null()
328+
}
329+
return types.Int64Value(value)
330+
}
331+
273332
func flattenResolvedProfileID(pool kernel.BrowserPool, diags *diag.Diagnostics) types.String {
274333
raw := pool.JSON.ProfileID.Raw()
275334
if raw != "" {

internal/datasources/browserpool/datasource_test.go

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestDataSourceMetadataSchemaAndConfigure(t *testing.T) {
4545

4646
var schema datasource.SchemaResponse
4747
ds.Schema(context.Background(), datasource.SchemaRequest{}, &schema)
48-
for _, name := range []string{"id", "name", "project_id", "size", "profile_id", "extension_ids", "proxy_id", "headless", "kiosk_mode", "stealth"} {
48+
for _, name := range []string{"id", "name", "project_id", "size", "profile_id", "extension_ids", "proxy_id", "headless", "kiosk_mode", "stealth", "start_url", "timeout_seconds", "fill_rate_per_minute"} {
4949
if _, ok := schema.Schema.Attributes[name]; !ok {
5050
t.Fatalf("schema missing %s", name)
5151
}
@@ -143,7 +143,10 @@ func TestReadSetsTerraformState(t *testing.T) {
143143
"proxy_id":"proxy-1",
144144
"headless":true,
145145
"kiosk_mode":false,
146-
"stealth":true
146+
"stealth":true,
147+
"start_url":"chrome://newtab",
148+
"timeout_seconds":10,
149+
"fill_rate_per_minute":0
147150
}
148151
}`), nil
149152
},
@@ -180,6 +183,56 @@ func TestReadSetsTerraformState(t *testing.T) {
180183
if state.ProxyID.ValueString() != "proxy-1" || !state.Headless.ValueBool() || state.KioskMode.IsNull() || state.KioskMode.ValueBool() || !state.Stealth.ValueBool() {
181184
t.Fatalf("launch state = %#v", state)
182185
}
186+
if state.StartURL.ValueString() != "chrome://newtab" || state.TimeoutSeconds.ValueInt64() != 10 || state.FillRatePerMinute.IsNull() || state.FillRatePerMinute.IsUnknown() || state.FillRatePerMinute.ValueInt64() != 0 {
187+
t.Fatalf("warmup state = %#v", state)
188+
}
189+
}
190+
191+
func TestFlattenBrowserPoolWarmupConfigurationBoundaries(t *testing.T) {
192+
t.Parallel()
193+
194+
omitted, diags := flattenBrowserPool(*browserPoolFromJSON(`{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1}}`))
195+
if diags.HasError() {
196+
t.Fatalf("unexpected omitted-field diagnostics: %v", diags)
197+
}
198+
if !omitted.StartURL.IsNull() || !omitted.TimeoutSeconds.IsNull() || !omitted.FillRatePerMinute.IsNull() {
199+
t.Fatalf("omitted warmup state = %#v, want null values", omitted)
200+
}
201+
202+
boundary, diags := flattenBrowserPool(*browserPoolFromJSON(`{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":259200,"fill_rate_per_minute":0}}`))
203+
if diags.HasError() {
204+
t.Fatalf("unexpected boundary diagnostics: %v", diags)
205+
}
206+
if boundary.TimeoutSeconds.ValueInt64() != 259200 || boundary.FillRatePerMinute.IsNull() || boundary.FillRatePerMinute.IsUnknown() || boundary.FillRatePerMinute.ValueInt64() != 0 {
207+
t.Fatalf("boundary warmup state = %#v", boundary)
208+
}
209+
}
210+
211+
func TestFlattenBrowserPoolRejectsInvalidWarmupConfiguration(t *testing.T) {
212+
t.Parallel()
213+
214+
tests := map[string]string{
215+
"empty start URL": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"start_url":""}}`,
216+
"null start URL": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"start_url":null}}`,
217+
"non-string URL": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"start_url":1}}`,
218+
"null timeout": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":null}}`,
219+
"non-number timeout": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":"10"}}`,
220+
"timeout too low": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":9}}`,
221+
"timeout too high": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":259201}}`,
222+
"null fill rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"fill_rate_per_minute":null}}`,
223+
"non-number rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"fill_rate_per_minute":"0"}}`,
224+
"negative fill rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"fill_rate_per_minute":-1}}`,
225+
}
226+
227+
for name, body := range tests {
228+
t.Run(name, func(t *testing.T) {
229+
t.Parallel()
230+
_, diags := flattenBrowserPool(*browserPoolFromJSON(body))
231+
if !diags.HasError() {
232+
t.Fatal("expected diagnostics")
233+
}
234+
})
235+
}
183236
}
184237

185238
func TestFlattenBrowserPoolLaunchConfiguration(t *testing.T) {
@@ -422,28 +475,34 @@ func browserPoolFromJSON(body string) *kernel.BrowserPool {
422475
func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value {
423476
return tftypes.NewValue(
424477
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
425-
"id": tftypes.String,
426-
"name": tftypes.String,
427-
"project_id": tftypes.String,
428-
"size": tftypes.Number,
429-
"profile_id": tftypes.String,
430-
"extension_ids": tftypes.List{ElementType: tftypes.String},
431-
"proxy_id": tftypes.String,
432-
"headless": tftypes.Bool,
433-
"kiosk_mode": tftypes.Bool,
434-
"stealth": tftypes.Bool,
478+
"id": tftypes.String,
479+
"name": tftypes.String,
480+
"project_id": tftypes.String,
481+
"size": tftypes.Number,
482+
"profile_id": tftypes.String,
483+
"extension_ids": tftypes.List{ElementType: tftypes.String},
484+
"proxy_id": tftypes.String,
485+
"headless": tftypes.Bool,
486+
"kiosk_mode": tftypes.Bool,
487+
"stealth": tftypes.Bool,
488+
"start_url": tftypes.String,
489+
"timeout_seconds": tftypes.Number,
490+
"fill_rate_per_minute": tftypes.Number,
435491
}},
436492
map[string]tftypes.Value{
437-
"id": id,
438-
"name": name,
439-
"project_id": projectID,
440-
"size": tftypes.NewValue(tftypes.Number, nil),
441-
"profile_id": tftypes.NewValue(tftypes.String, nil),
442-
"extension_ids": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil),
443-
"proxy_id": tftypes.NewValue(tftypes.String, nil),
444-
"headless": tftypes.NewValue(tftypes.Bool, nil),
445-
"kiosk_mode": tftypes.NewValue(tftypes.Bool, nil),
446-
"stealth": tftypes.NewValue(tftypes.Bool, nil),
493+
"id": id,
494+
"name": name,
495+
"project_id": projectID,
496+
"size": tftypes.NewValue(tftypes.Number, nil),
497+
"profile_id": tftypes.NewValue(tftypes.String, nil),
498+
"extension_ids": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil),
499+
"proxy_id": tftypes.NewValue(tftypes.String, nil),
500+
"headless": tftypes.NewValue(tftypes.Bool, nil),
501+
"kiosk_mode": tftypes.NewValue(tftypes.Bool, nil),
502+
"stealth": tftypes.NewValue(tftypes.Bool, nil),
503+
"start_url": tftypes.NewValue(tftypes.String, nil),
504+
"timeout_seconds": tftypes.NewValue(tftypes.Number, nil),
505+
"fill_rate_per_minute": tftypes.NewValue(tftypes.Number, nil),
447506
},
448507
)
449508
}

0 commit comments

Comments
 (0)