diff --git a/docs/data-sources/browser_pool.md b/docs/data-sources/browser_pool.md index 1e7b1e9..9ff97af 100644 --- a/docs/data-sources/browser_pool.md +++ b/docs/data-sources/browser_pool.md @@ -24,5 +24,9 @@ Lookup durable Kernel browser pool configuration. ### Read-Only - `extension_ids` (List of String) Resolved extension IDs attached to the pool, in load order. +- `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. +- `stealth` (Boolean) Whether browsers launch in stealth mode. diff --git a/internal/datasources/browserpool/datasource.go b/internal/datasources/browserpool/datasource.go index aa9fa0b..45267fb 100644 --- a/internal/datasources/browserpool/datasource.go +++ b/internal/datasources/browserpool/datasource.go @@ -40,6 +40,10 @@ type browserPoolModel struct { 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"` } func NewDataSource() datasource.DataSource { @@ -88,6 +92,22 @@ func (d *browserPoolDataSource) Schema(_ context.Context, _ datasource.SchemaReq ElementType: types.StringType, MarkdownDescription: "Resolved extension IDs attached to the pool, in load order.", }, + "proxy_id": dschema.StringAttribute{ + Computed: true, + MarkdownDescription: "Proxy ID attached to browsers in the pool, if any.", + }, + "headless": dschema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Whether browsers use a headless image.", + }, + "kiosk_mode": dschema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Whether browsers launch in kiosk mode.", + }, + "stealth": dschema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Whether browsers launch in stealth mode.", + }, }, } } @@ -214,15 +234,42 @@ func flattenBrowserPool(pool kernel.BrowserPool) (browserPoolModel, diag.Diagnos return browserPoolModel{}, diags } + config := pool.BrowserPoolConfig return browserPoolModel{ ID: types.StringValue(pool.ID), Name: name, - Size: types.Int64Value(pool.BrowserPoolConfig.Size), + 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), }, diags } +func flattenOptionalString(field, raw string, valid bool, value string, diags *diag.Diagnostics) types.String { + if raw == "" { + return types.StringNull() + } + if !datasources.ValidResponseString(raw, valid, value) { + datasources.AddInvalidResponseField(diags, "Browser Pool", field) + return types.StringNull() + } + return types.StringValue(value) +} + +func flattenOptionalBool(field, raw string, valid bool, value bool, diags *diag.Diagnostics) types.Bool { + if raw == "" { + return types.BoolNull() + } + if !validResponseBool(raw, valid, value) { + datasources.AddInvalidResponseField(diags, "Browser Pool", field) + return types.BoolNull() + } + return types.BoolValue(value) +} + func flattenResolvedProfileID(pool kernel.BrowserPool, diags *diag.Diagnostics) types.String { raw := pool.JSON.ProfileID.Raw() if raw != "" { @@ -303,3 +350,11 @@ func validResponseInt64(raw string, valid bool, value int64) bool { var decoded int64 return json.Unmarshal([]byte(raw), &decoded) == nil && decoded == value } + +func validResponseBool(raw string, valid bool, value bool) bool { + if !datasources.FieldPresent(raw) || !valid { + return false + } + var decoded bool + return json.Unmarshal([]byte(raw), &decoded) == nil && decoded == value +} diff --git a/internal/datasources/browserpool/datasource_test.go b/internal/datasources/browserpool/datasource_test.go index b2b82d8..3165e98 100644 --- a/internal/datasources/browserpool/datasource_test.go +++ b/internal/datasources/browserpool/datasource_test.go @@ -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"} { + for _, name := range []string{"id", "name", "project_id", "size", "profile_id", "extension_ids", "proxy_id", "headless", "kiosk_mode", "stealth"} { if _, ok := schema.Schema.Attributes[name]; !ok { t.Fatalf("schema missing %s", name) } @@ -86,6 +86,10 @@ func TestDataSourceSchemaSemantics(t *testing.T) { assertAttributeMode(t, resp.Schema, "size", false, true) assertAttributeMode(t, resp.Schema, "profile_id", false, true) assertAttributeMode(t, resp.Schema, "extension_ids", false, true) + assertAttributeMode(t, resp.Schema, "proxy_id", false, true) + assertAttributeMode(t, resp.Schema, "headless", false, true) + assertAttributeMode(t, resp.Schema, "kiosk_mode", false, true) + assertAttributeMode(t, resp.Schema, "stealth", false, true) projectID := resp.Schema.Attributes["project_id"].(dschema.StringAttribute) if !validateProjectID(projectID.Validators, "").HasError() { @@ -182,7 +186,18 @@ func TestReadSetsTerraformState(t *testing.T) { ds := newDataSourceWithClient(fakeBrowserPoolClient{ get: func(context.Context, string, string) (*kernel.BrowserPool, error) { - return browserPoolForTest("pool-1", "Pool", 2), nil + return browserPoolFromJSON(`{ + "id":"pool-1", + "name":"Pool", + "extension_ids":[], + "browser_pool_config":{ + "size":2, + "proxy_id":"proxy-1", + "headless":true, + "kiosk_mode":false, + "stealth":true + } + }`), nil }, }) var schema datasource.SchemaResponse @@ -214,6 +229,66 @@ func TestReadSetsTerraformState(t *testing.T) { t.Fatalf("profile_id = %v, want null", state.ProfileID) } assertBrowserPoolStringList(t, state.ExtensionIDs, nil) + if state.ProxyID.ValueString() != "proxy-1" || !state.Headless.ValueBool() || state.KioskMode.IsNull() || state.KioskMode.ValueBool() || !state.Stealth.ValueBool() { + t.Fatalf("launch state = %#v", state) + } +} + +func TestFlattenBrowserPoolLaunchConfiguration(t *testing.T) { + t.Parallel() + + state, diags := flattenBrowserPool(*browserPoolFromJSON(`{ + "id":"pool-1", + "extension_ids":[], + "browser_pool_config":{ + "size":1, + "proxy_id":"proxy-1", + "headless":true, + "kiosk_mode":false, + "stealth":true + } + }`)) + if diags.HasError() { + t.Fatalf("unexpected diagnostics: %v", diags) + } + if state.ProxyID.ValueString() != "proxy-1" { + t.Fatalf("proxy_id = %q, want proxy-1", state.ProxyID.ValueString()) + } + if !state.Headless.ValueBool() { + t.Fatal("headless = false, want true") + } + if state.KioskMode.IsNull() || state.KioskMode.ValueBool() { + t.Fatalf("kiosk_mode = %v, want known false", state.KioskMode) + } + if !state.Stealth.ValueBool() { + t.Fatal("stealth = false, want true") + } +} + +func TestFlattenBrowserPoolRejectsInvalidLaunchConfiguration(t *testing.T) { + t.Parallel() + + tests := map[string]string{ + "empty proxy ID": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"proxy_id":""}}`, + "null proxy ID": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"proxy_id":null}}`, + "non-string proxy": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"proxy_id":1}}`, + "null headless": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"headless":null}}`, + "non-bool headless": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"headless":"true"}}`, + "null kiosk": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"kiosk_mode":null}}`, + "non-bool kiosk": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"kiosk_mode":1}}`, + "null stealth": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"stealth":null}}`, + "non-bool stealth": `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1,"stealth":{}}}`, + } + + 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 TestFlattenBrowserPoolResolvedReferences(t *testing.T) { @@ -405,6 +480,10 @@ func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value { "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, }}, map[string]tftypes.Value{ "id": id, @@ -413,6 +492,10 @@ func browserPoolConfigValue(id, name, projectID tftypes.Value) tftypes.Value { "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), }, ) }