Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/data-sources/browser_pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ Lookup durable Kernel browser pool configuration.
### Read-Only

- `extension_ids` (List of String) Resolved extension IDs attached to the pool, in load order.
- `fill_rate_per_minute` (Number) Percentage of the pool filled per minute.
- `headless` (Boolean) Whether browsers use a headless image.
- `kiosk_mode` (Boolean) Whether browsers launch in kiosk mode.
- `profile_id` (String) Resolved profile ID attached to the pool, if any.
- `proxy_id` (String) Proxy ID attached to browsers in the pool, if any.
- `size` (Number) Number of browsers maintained in the pool.
- `start_url` (String) URL opened when a browser is warmed into the pool, if configured.
- `stealth` (Boolean) Whether browsers launch in stealth mode.
- `timeout_seconds` (Number) Default idle timeout in seconds for acquired browsers.
97 changes: 78 additions & 19 deletions internal/datasources/browserpool/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ var (
_ datasource.DataSourceWithConfigure = (*browserPoolDataSource)(nil)
)

const (
minBrowserPoolTimeoutSeconds = 10
maxBrowserPoolTimeoutSeconds = 259200
minBrowserPoolFillRate = 0
)

type browserPoolClient interface {
DefaultProjectID() string
GetBrowserPool(context.Context, string, string) (*kernel.BrowserPool, error)
Expand All @@ -34,16 +40,19 @@ type browserPoolDataSource struct {
}

type browserPoolModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ProjectID types.String `tfsdk:"project_id"`
Size types.Int64 `tfsdk:"size"`
ProfileID types.String `tfsdk:"profile_id"`
ExtensionIDs types.List `tfsdk:"extension_ids"`
ProxyID types.String `tfsdk:"proxy_id"`
Headless types.Bool `tfsdk:"headless"`
KioskMode types.Bool `tfsdk:"kiosk_mode"`
Stealth types.Bool `tfsdk:"stealth"`
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ProjectID types.String `tfsdk:"project_id"`
Size types.Int64 `tfsdk:"size"`
ProfileID types.String `tfsdk:"profile_id"`
ExtensionIDs types.List `tfsdk:"extension_ids"`
ProxyID types.String `tfsdk:"proxy_id"`
Headless types.Bool `tfsdk:"headless"`
KioskMode types.Bool `tfsdk:"kiosk_mode"`
Stealth types.Bool `tfsdk:"stealth"`
StartURL types.String `tfsdk:"start_url"`
TimeoutSeconds types.Int64 `tfsdk:"timeout_seconds"`
FillRatePerMinute types.Int64 `tfsdk:"fill_rate_per_minute"`
}

func NewDataSource() datasource.DataSource {
Expand Down Expand Up @@ -108,6 +117,18 @@ func (d *browserPoolDataSource) Schema(_ context.Context, _ datasource.SchemaReq
Computed: true,
MarkdownDescription: "Whether browsers launch in stealth mode.",
},
"start_url": dschema.StringAttribute{
Computed: true,
MarkdownDescription: "URL opened when a browser is warmed into the pool, if configured.",
},
"timeout_seconds": dschema.Int64Attribute{
Computed: true,
MarkdownDescription: "Default idle timeout in seconds for acquired browsers.",
},
"fill_rate_per_minute": dschema.Int64Attribute{
Computed: true,
MarkdownDescription: "Percentage of the pool filled per minute.",
},
},
}
}
Expand Down Expand Up @@ -236,15 +257,18 @@ func flattenBrowserPool(pool kernel.BrowserPool) (browserPoolModel, diag.Diagnos

config := pool.BrowserPoolConfig
return browserPoolModel{
ID: types.StringValue(pool.ID),
Name: name,
Size: types.Int64Value(config.Size),
ProfileID: flattenResolvedProfileID(pool, &diags),
ExtensionIDs: flattenResolvedExtensionIDs(pool, &diags),
ProxyID: flattenOptionalString("browser_pool_config.proxy_id", config.JSON.ProxyID.Raw(), config.JSON.ProxyID.Valid(), config.ProxyID, &diags),
Headless: flattenOptionalBool("browser_pool_config.headless", config.JSON.Headless.Raw(), config.JSON.Headless.Valid(), config.Headless, &diags),
KioskMode: flattenOptionalBool("browser_pool_config.kiosk_mode", config.JSON.KioskMode.Raw(), config.JSON.KioskMode.Valid(), config.KioskMode, &diags),
Stealth: flattenOptionalBool("browser_pool_config.stealth", config.JSON.Stealth.Raw(), config.JSON.Stealth.Valid(), config.Stealth, &diags),
ID: types.StringValue(pool.ID),
Name: name,
Size: types.Int64Value(config.Size),
ProfileID: flattenResolvedProfileID(pool, &diags),
ExtensionIDs: flattenResolvedExtensionIDs(pool, &diags),
ProxyID: flattenOptionalString("browser_pool_config.proxy_id", config.JSON.ProxyID.Raw(), config.JSON.ProxyID.Valid(), config.ProxyID, &diags),
Headless: flattenOptionalBool("browser_pool_config.headless", config.JSON.Headless.Raw(), config.JSON.Headless.Valid(), config.Headless, &diags),
KioskMode: flattenOptionalBool("browser_pool_config.kiosk_mode", config.JSON.KioskMode.Raw(), config.JSON.KioskMode.Valid(), config.KioskMode, &diags),
Stealth: flattenOptionalBool("browser_pool_config.stealth", config.JSON.Stealth.Raw(), config.JSON.Stealth.Valid(), config.Stealth, &diags),
StartURL: flattenOptionalString("browser_pool_config.start_url", config.JSON.StartURL.Raw(), config.JSON.StartURL.Valid(), config.StartURL, &diags),
TimeoutSeconds: flattenTimeoutSeconds(config.JSON.TimeoutSeconds.Raw(), config.JSON.TimeoutSeconds.Valid(), config.TimeoutSeconds, &diags),
FillRatePerMinute: flattenFillRatePerMinute(config.JSON.FillRatePerMinute.Raw(), config.JSON.FillRatePerMinute.Valid(), config.FillRatePerMinute, &diags),
}, diags
}

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

func flattenTimeoutSeconds(raw string, valid bool, value int64, diags *diag.Diagnostics) types.Int64 {
result := flattenOptionalInt64("browser_pool_config.timeout_seconds", raw, valid, value, diags)
if result.IsNull() {
return result
}
if value < minBrowserPoolTimeoutSeconds || value > maxBrowserPoolTimeoutSeconds {
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.timeout_seconds")
return types.Int64Null()
}
return result
}

func flattenFillRatePerMinute(raw string, valid bool, value int64, diags *diag.Diagnostics) types.Int64 {
result := flattenOptionalInt64("browser_pool_config.fill_rate_per_minute", raw, valid, value, diags)
if result.IsNull() {
return result
}
if value < minBrowserPoolFillRate {
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.fill_rate_per_minute")
return types.Int64Null()
}
return result
}

func flattenOptionalInt64(field, raw string, valid bool, value int64, diags *diag.Diagnostics) types.Int64 {
if raw == "" {
return types.Int64Null()
}
if !validResponseInt64(raw, valid, value) {
datasources.AddInvalidResponseField(diags, "Browser Pool", field)
return types.Int64Null()
}
return types.Int64Value(value)
}

func flattenResolvedProfileID(pool kernel.BrowserPool, diags *diag.Diagnostics) types.String {
raw := pool.JSON.ProfileID.Raw()
if raw != "" {
Expand Down
106 changes: 84 additions & 22 deletions internal/datasources/browserpool/datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestDataSourceMetadataSchemaAndConfigure(t *testing.T) {

var schema datasource.SchemaResponse
ds.Schema(context.Background(), datasource.SchemaRequest{}, &schema)
for _, name := range []string{"id", "name", "project_id", "size", "profile_id", "extension_ids", "proxy_id", "headless", "kiosk_mode", "stealth"} {
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"} {
if _, ok := schema.Schema.Attributes[name]; !ok {
t.Fatalf("schema missing %s", name)
}
Expand Down Expand Up @@ -90,6 +90,9 @@ func TestDataSourceSchemaSemantics(t *testing.T) {
assertAttributeMode(t, resp.Schema, "headless", false, true)
assertAttributeMode(t, resp.Schema, "kiosk_mode", false, true)
assertAttributeMode(t, resp.Schema, "stealth", false, true)
assertAttributeMode(t, resp.Schema, "start_url", false, true)
assertAttributeMode(t, resp.Schema, "timeout_seconds", false, true)
assertAttributeMode(t, resp.Schema, "fill_rate_per_minute", false, true)

projectID := resp.Schema.Attributes["project_id"].(dschema.StringAttribute)
if !validateProjectID(projectID.Validators, "").HasError() {
Expand Down Expand Up @@ -195,7 +198,10 @@ func TestReadSetsTerraformState(t *testing.T) {
"proxy_id":"proxy-1",
"headless":true,
"kiosk_mode":false,
"stealth":true
"stealth":true,
"start_url":"chrome://newtab",
"timeout_seconds":10,
"fill_rate_per_minute":0
}
}`), nil
},
Expand Down Expand Up @@ -232,6 +238,56 @@ func TestReadSetsTerraformState(t *testing.T) {
if state.ProxyID.ValueString() != "proxy-1" || !state.Headless.ValueBool() || state.KioskMode.IsNull() || state.KioskMode.ValueBool() || !state.Stealth.ValueBool() {
t.Fatalf("launch state = %#v", state)
}
if state.StartURL.ValueString() != "chrome://newtab" || state.TimeoutSeconds.ValueInt64() != 10 || state.FillRatePerMinute.IsNull() || state.FillRatePerMinute.IsUnknown() || state.FillRatePerMinute.ValueInt64() != 0 {
t.Fatalf("warmup state = %#v", state)
}
}

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

omitted, diags := flattenBrowserPool(*browserPoolFromJSON(`{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1}}`))
if diags.HasError() {
t.Fatalf("unexpected omitted-field diagnostics: %v", diags)
}
if !omitted.StartURL.IsNull() || !omitted.TimeoutSeconds.IsNull() || !omitted.FillRatePerMinute.IsNull() {
t.Fatalf("omitted warmup state = %#v, want null values", omitted)
}

boundary, diags := flattenBrowserPool(*browserPoolFromJSON(`{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":259200,"fill_rate_per_minute":0}}`))
if diags.HasError() {
t.Fatalf("unexpected boundary diagnostics: %v", diags)
}
if boundary.TimeoutSeconds.ValueInt64() != 259200 || boundary.FillRatePerMinute.IsNull() || boundary.FillRatePerMinute.IsUnknown() || boundary.FillRatePerMinute.ValueInt64() != 0 {
t.Fatalf("boundary warmup state = %#v", boundary)
}
}

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

tests := map[string]string{
"empty start URL": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"start_url":""}}`,
"null start URL": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"start_url":null}}`,
"non-string URL": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"start_url":1}}`,
"null timeout": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":null}}`,
"non-number timeout": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":"10"}}`,
"timeout too low": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":9}}`,
"timeout too high": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"timeout_seconds":259201}}`,
"null fill rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"fill_rate_per_minute":null}}`,
"non-number rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"fill_rate_per_minute":"0"}}`,
"negative fill rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"fill_rate_per_minute":-1}}`,
}

for name, body := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, diags := flattenBrowserPool(*browserPoolFromJSON(body))
if !diags.HasError() {
t.Fatal("expected diagnostics")
}
})
}
}

func TestFlattenBrowserPoolLaunchConfiguration(t *testing.T) {
Expand Down Expand Up @@ -474,28 +530,34 @@ func browserPoolFromJSON(body string) *kernel.BrowserPool {
func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value {
return tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
"name": tftypes.String,
"project_id": tftypes.String,
"size": tftypes.Number,
"profile_id": tftypes.String,
"extension_ids": tftypes.List{ElementType: tftypes.String},
"proxy_id": tftypes.String,
"headless": tftypes.Bool,
"kiosk_mode": tftypes.Bool,
"stealth": tftypes.Bool,
"id": tftypes.String,
"name": tftypes.String,
"project_id": tftypes.String,
"size": tftypes.Number,
"profile_id": tftypes.String,
"extension_ids": tftypes.List{ElementType: tftypes.String},
"proxy_id": tftypes.String,
"headless": tftypes.Bool,
"kiosk_mode": tftypes.Bool,
"stealth": tftypes.Bool,
"start_url": tftypes.String,
"timeout_seconds": tftypes.Number,
"fill_rate_per_minute": tftypes.Number,
}},
map[string]tftypes.Value{
"id": id,
"name": name,
"project_id": projectID,
"size": tftypes.NewValue(tftypes.Number, nil),
"profile_id": tftypes.NewValue(tftypes.String, nil),
"extension_ids": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil),
"proxy_id": tftypes.NewValue(tftypes.String, nil),
"headless": tftypes.NewValue(tftypes.Bool, nil),
"kiosk_mode": tftypes.NewValue(tftypes.Bool, nil),
"stealth": tftypes.NewValue(tftypes.Bool, nil),
"id": id,
"name": name,
"project_id": projectID,
"size": tftypes.NewValue(tftypes.Number, nil),
"profile_id": tftypes.NewValue(tftypes.String, nil),
"extension_ids": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil),
"proxy_id": tftypes.NewValue(tftypes.String, nil),
"headless": tftypes.NewValue(tftypes.Bool, nil),
"kiosk_mode": tftypes.NewValue(tftypes.Bool, nil),
"stealth": tftypes.NewValue(tftypes.Bool, nil),
"start_url": tftypes.NewValue(tftypes.String, nil),
"timeout_seconds": tftypes.NewValue(tftypes.Number, nil),
"fill_rate_per_minute": tftypes.NewValue(tftypes.Number, nil),
},
)
}
Expand Down