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
10 changes: 10 additions & 0 deletions docs/data-sources/browser_pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ Lookup durable Kernel browser pool configuration.
- `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.
- `viewport` (Attributes) Browser viewport configured for the pool, if any. (see [below for nested schema](#nestedatt--viewport))

<a id="nestedatt--viewport"></a>
### Nested Schema for `viewport`

Read-Only:

- `height` (Number) Browser window height in pixels.
- `refresh_rate` (Number) Display refresh rate in Hz, if configured.
- `width` (Number) Browser window width in pixels.
55 changes: 55 additions & 0 deletions internal/datasources/browserpool/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
minBrowserPoolTimeoutSeconds = 10
maxBrowserPoolTimeoutSeconds = 259200
minBrowserPoolFillRate = 0
minBrowserPoolViewportValue = 1
)

type browserPoolClient interface {
Expand All @@ -53,6 +54,7 @@ type browserPoolModel struct {
StartURL types.String `tfsdk:"start_url"`
TimeoutSeconds types.Int64 `tfsdk:"timeout_seconds"`
FillRatePerMinute types.Int64 `tfsdk:"fill_rate_per_minute"`
Viewport types.Object `tfsdk:"viewport"`
}

func NewDataSource() datasource.DataSource {
Expand Down Expand Up @@ -129,6 +131,15 @@ func (d *browserPoolDataSource) Schema(_ context.Context, _ datasource.SchemaReq
Computed: true,
MarkdownDescription: "Percentage of the pool filled per minute.",
},
"viewport": dschema.SingleNestedAttribute{
Computed: true,
MarkdownDescription: "Browser viewport configured for the pool, if any.",
Attributes: map[string]dschema.Attribute{
"width": dschema.Int64Attribute{Computed: true, MarkdownDescription: "Browser window width in pixels."},
"height": dschema.Int64Attribute{Computed: true, MarkdownDescription: "Browser window height in pixels."},
"refresh_rate": dschema.Int64Attribute{Computed: true, MarkdownDescription: "Display refresh rate in Hz, if configured."},
},
},
},
}
}
Expand Down Expand Up @@ -269,6 +280,7 @@ func flattenBrowserPool(pool kernel.BrowserPool) (browserPoolModel, diag.Diagnos
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),
Viewport: flattenViewport(config.JSON.Viewport.Raw(), config.JSON.Viewport.Valid(), config.Viewport, &diags),
}, diags
}

Expand Down Expand Up @@ -329,6 +341,49 @@ func flattenOptionalInt64(field, raw string, valid bool, value int64, diags *dia
return types.Int64Value(value)
}

func flattenViewport(raw string, valid bool, viewport shared.BrowserViewport, diags *diag.Diagnostics) types.Object {
if raw == "" {
return types.ObjectNull(viewportAttributeTypes())
}
if !datasources.FieldPresent(raw) || !valid {
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.viewport")
return types.ObjectNull(viewportAttributeTypes())
}

diagnosticsBefore := len(*diags)
width := flattenRequiredPositiveInt64("browser_pool_config.viewport.width", viewport.JSON.Width.Raw(), viewport.JSON.Width.Valid(), viewport.Width, diags)
height := flattenRequiredPositiveInt64("browser_pool_config.viewport.height", viewport.JSON.Height.Raw(), viewport.JSON.Height.Valid(), viewport.Height, diags)
refreshRate := types.Int64Null()
if viewport.JSON.RefreshRate.Raw() != "" {
refreshRate = flattenRequiredPositiveInt64("browser_pool_config.viewport.refresh_rate", viewport.JSON.RefreshRate.Raw(), viewport.JSON.RefreshRate.Valid(), viewport.RefreshRate, diags)
}
if len(*diags) > diagnosticsBefore {
return types.ObjectNull(viewportAttributeTypes())
}

return types.ObjectValueMust(viewportAttributeTypes(), map[string]attr.Value{
"width": width,
"height": height,
"refresh_rate": refreshRate,
})
}

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

func viewportAttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"width": types.Int64Type,
"height": types.Int64Type,
"refresh_rate": types.Int64Type,
}
}

func flattenResolvedProfileID(pool kernel.BrowserPool, diags *diag.Diagnostics) types.String {
raw := pool.JSON.ProfileID.Raw()
if raw != "" {
Expand Down
91 changes: 88 additions & 3 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", "start_url", "timeout_seconds", "fill_rate_per_minute"} {
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", "viewport"} {
if _, ok := schema.Schema.Attributes[name]; !ok {
t.Fatalf("schema missing %s", name)
}
Expand Down Expand Up @@ -93,6 +93,12 @@ func TestDataSourceSchemaSemantics(t *testing.T) {
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)
assertAttributeMode(t, resp.Schema, "viewport", false, true)

viewport := resp.Schema.Attributes["viewport"].(dschema.SingleNestedAttribute)
for _, name := range []string{"width", "height", "refresh_rate"} {
assertAttributeMapMode(t, viewport.Attributes, name, false, true)
}

projectID := resp.Schema.Attributes["project_id"].(dschema.StringAttribute)
if !validateProjectID(projectID.Validators, "").HasError() {
Expand All @@ -105,7 +111,12 @@ func TestDataSourceSchemaSemantics(t *testing.T) {

func assertAttributeMode(t *testing.T, schema dschema.Schema, name string, optional, computed bool) {
t.Helper()
attribute, ok := schema.Attributes[name]
assertAttributeMapMode(t, schema.Attributes, name, optional, computed)
}

func assertAttributeMapMode(t *testing.T, attributes map[string]dschema.Attribute, name string, optional, computed bool) {
t.Helper()
attribute, ok := attributes[name]
if !ok {
t.Fatalf("schema missing %s", name)
}
Expand Down Expand Up @@ -201,7 +212,8 @@ func TestReadSetsTerraformState(t *testing.T) {
"stealth":true,
"start_url":"chrome://newtab",
"timeout_seconds":10,
"fill_rate_per_minute":0
"fill_rate_per_minute":0,
"viewport":{"width":1280,"height":800,"refresh_rate":60}
}
}`), nil
},
Expand Down Expand Up @@ -241,6 +253,61 @@ func TestReadSetsTerraformState(t *testing.T) {
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)
}
assertBrowserPoolViewport(t, state.Viewport, 1280, 800, types.Int64Value(60))
}

func TestFlattenBrowserPoolViewportOptionalFields(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 viewport diagnostics: %v", diags)
}
if !omitted.Viewport.IsNull() || len(omitted.Viewport.AttributeTypes(t.Context())) != 3 {
t.Fatalf("omitted viewport = %#v, want typed null", omitted.Viewport)
}

withoutRefreshRate, diags := flattenBrowserPool(*browserPoolFromJSON(`{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":1280,"height":800}}}`))
if diags.HasError() {
t.Fatalf("unexpected viewport diagnostics: %v", diags)
}
assertBrowserPoolViewport(t, withoutRefreshRate.Viewport, 1280, 800, types.Int64Null())
}

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

state, diags := flattenBrowserPool(*browserPoolFromJSON(`{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":1,"height":1,"refresh_rate":1}}}`))
if diags.HasError() {
t.Fatalf("unexpected minimum viewport diagnostics: %v", diags)
}
assertBrowserPoolViewport(t, state.Viewport, 1, 1, types.Int64Value(1))
}

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

tests := map[string]string{
"null viewport": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":null}}`,
"non-object viewport": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":[]}}`,
"missing width": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"height":800}}}`,
"missing height": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":1280}}}`,
"zero width": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":0,"height":800}}}`,
"negative height": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":1280,"height":-1}}}`,
"null refresh rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":1280,"height":800,"refresh_rate":null}}}`,
"zero refresh rate": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":1280,"height":800,"refresh_rate":0}}}`,
"non-number dimension": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"viewport":{"width":"1280","height":800}}}`,
}

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 TestFlattenBrowserPoolWarmupConfigurationBoundaries(t *testing.T) {
Expand Down Expand Up @@ -528,6 +595,11 @@ func browserPoolFromJSON(body string) *kernel.BrowserPool {
}

func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value {
viewportType := tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"width": tftypes.Number,
"height": tftypes.Number,
"refresh_rate": tftypes.Number,
}}
return tftypes.NewValue(
tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
Expand All @@ -543,6 +615,7 @@ func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value {
"start_url": tftypes.String,
"timeout_seconds": tftypes.Number,
"fill_rate_per_minute": tftypes.Number,
"viewport": viewportType,
}},
map[string]tftypes.Value{
"id": id,
Expand All @@ -558,10 +631,22 @@ func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value {
"start_url": tftypes.NewValue(tftypes.String, nil),
"timeout_seconds": tftypes.NewValue(tftypes.Number, nil),
"fill_rate_per_minute": tftypes.NewValue(tftypes.Number, nil),
"viewport": tftypes.NewValue(viewportType, nil),
},
)
}

func assertBrowserPoolViewport(t *testing.T, viewport types.Object, width, height int64, refreshRate types.Int64) {
t.Helper()
if viewport.IsNull() || viewport.IsUnknown() {
t.Fatalf("viewport = %#v, want known object", viewport)
}
attributes := viewport.Attributes()
if !attributes["width"].(types.Int64).Equal(types.Int64Value(width)) || !attributes["height"].(types.Int64).Equal(types.Int64Value(height)) || !attributes["refresh_rate"].(types.Int64).Equal(refreshRate) {
t.Fatalf("viewport = %#v, want %dx%d refresh %v", viewport, width, height, refreshRate)
}
}

func assertBrowserPoolStringList(t *testing.T, got types.List, want []string) {
t.Helper()
if got.IsNull() || got.IsUnknown() {
Expand Down