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
2 changes: 2 additions & 0 deletions docs/data-sources/browser_pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ Lookup durable Kernel browser pool configuration.

### Read-Only

- `extension_ids` (List of String) Resolved extension IDs attached to the pool, in load order.
- `profile_id` (String) Resolved profile ID attached to the pool, if any.
- `size` (Number) Number of browsers maintained in the pool.
103 changes: 96 additions & 7 deletions internal/datasources/browserpool/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package browserpool
import (
"context"
"encoding/json"
"fmt"
"strconv"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
kernel "github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/shared"
"github.com/kernel/terraform-provider-kernel/internal/datasources"
"github.com/kernel/terraform-provider-kernel/internal/projectscope"
)
Expand All @@ -31,10 +34,12 @@ 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"`
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"`
}

func NewDataSource() datasource.DataSource {
Expand Down Expand Up @@ -74,6 +79,15 @@ func (d *browserPoolDataSource) Schema(_ context.Context, _ datasource.SchemaReq
Computed: true,
MarkdownDescription: "Number of browsers maintained in the pool.",
},
"profile_id": dschema.StringAttribute{
Computed: true,
MarkdownDescription: "Resolved profile ID attached to the pool, if any.",
},
"extension_ids": dschema.ListAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "Resolved extension IDs attached to the pool, in load order.",
},
},
}
}
Expand Down Expand Up @@ -201,12 +215,87 @@ func flattenBrowserPool(pool kernel.BrowserPool) (browserPoolModel, diag.Diagnos
}

return browserPoolModel{
ID: types.StringValue(pool.ID),
Name: name,
Size: types.Int64Value(pool.BrowserPoolConfig.Size),
ID: types.StringValue(pool.ID),
Name: name,
Size: types.Int64Value(pool.BrowserPoolConfig.Size),
ProfileID: flattenResolvedProfileID(pool, &diags),
ExtensionIDs: flattenResolvedExtensionIDs(pool, &diags),
}, diags
}

func flattenResolvedProfileID(pool kernel.BrowserPool, diags *diag.Diagnostics) types.String {
raw := pool.JSON.ProfileID.Raw()
if raw != "" {
if !datasources.FieldPresent(raw) {
datasources.AddInvalidResponseField(diags, "Browser Pool", "profile_id")
return types.StringNull()
}
if !datasources.ValidResponseString(raw, pool.JSON.ProfileID.Valid(), pool.ProfileID) {
datasources.AddInvalidResponseField(diags, "Browser Pool", "profile_id")
return types.StringNull()
}
return types.StringValue(pool.ProfileID)
}

profile := pool.BrowserPoolConfig.Profile
if !datasources.FieldPresent(pool.BrowserPoolConfig.JSON.Profile.Raw()) {
return types.StringNull()
}
if !datasources.ValidResponseString(profile.JSON.ID.Raw(), profile.JSON.ID.Valid(), profile.ID) {
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.profile.id")
return types.StringNull()
}
return types.StringValue(profile.ID)
}

func flattenResolvedExtensionIDs(pool kernel.BrowserPool, diags *diag.Diagnostics) types.List {
raw := pool.JSON.ExtensionIDs.Raw()
if raw != "" {
return flattenStringList("extension_ids", raw, pool.JSON.ExtensionIDs.Valid(), pool.ExtensionIDs, diags)
}

config := pool.BrowserPoolConfig
if !datasources.FieldPresent(config.JSON.Extensions.Raw()) {
return types.ListValueMust(types.StringType, nil)
}
return flattenExtensionIDs(config.JSON.Extensions.Valid(), config.Extensions, diags)
}

func flattenStringList(field, raw string, valid bool, values []string, diags *diag.Diagnostics) types.List {
var decoded []string
if !datasources.FieldPresent(raw) || !valid || json.Unmarshal([]byte(raw), &decoded) != nil || len(decoded) != len(values) {
datasources.AddInvalidResponseField(diags, "Browser Pool", field)
return types.ListNull(types.StringType)
}

elements := make([]attr.Value, 0, len(values))
for index, value := range values {
if value == "" || decoded[index] != value {
datasources.AddInvalidResponseField(diags, "Browser Pool", fmt.Sprintf("%s[%d]", field, index))
return types.ListNull(types.StringType)
}
elements = append(elements, types.StringValue(value))
}
return types.ListValueMust(types.StringType, elements)
}

func flattenExtensionIDs(valid bool, extensions []shared.BrowserExtension, diags *diag.Diagnostics) types.List {
if !valid {
datasources.AddInvalidResponseField(diags, "Browser Pool", "browser_pool_config.extensions")
return types.ListNull(types.StringType)
}

elements := make([]attr.Value, 0, len(extensions))
for index, extension := range extensions {
if !datasources.ValidResponseString(extension.JSON.ID.Raw(), extension.JSON.ID.Valid(), extension.ID) {
datasources.AddInvalidResponseField(diags, "Browser Pool", fmt.Sprintf("browser_pool_config.extensions[%d].id", index))
return types.ListNull(types.StringType)
}
elements = append(elements, types.StringValue(extension.ID))
}
return types.ListValueMust(types.StringType, elements)
}

func validResponseInt64(raw string, valid bool, value int64) bool {
if !datasources.FieldPresent(raw) || !valid {
return false
Expand Down
133 changes: 123 additions & 10 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"} {
for _, name := range []string{"id", "name", "project_id", "size", "profile_id", "extension_ids"} {
if _, ok := schema.Schema.Attributes[name]; !ok {
t.Fatalf("schema missing %s", name)
}
Expand Down Expand Up @@ -84,6 +84,8 @@ func TestDataSourceSchemaSemantics(t *testing.T) {
assertAttributeMode(t, resp.Schema, "name", true, true)
assertAttributeMode(t, resp.Schema, "project_id", true, false)
assertAttributeMode(t, resp.Schema, "size", false, true)
assertAttributeMode(t, resp.Schema, "profile_id", false, true)
assertAttributeMode(t, resp.Schema, "extension_ids", false, true)

projectID := resp.Schema.Attributes["project_id"].(dschema.StringAttribute)
if !validateProjectID(projectID.Validators, "").HasError() {
Expand Down Expand Up @@ -208,6 +210,96 @@ func TestReadSetsTerraformState(t *testing.T) {
if state.ID.ValueString() != "pool-1" || state.Name.ValueString() != "Pool" || state.Size.ValueInt64() != 2 {
t.Fatalf("state = %#v", state)
}
if !state.ProfileID.IsNull() {
t.Fatalf("profile_id = %v, want null", state.ProfileID)
}
assertBrowserPoolStringList(t, state.ExtensionIDs, nil)
}

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

tests := map[string]struct {
body string
wantProfileID string
wantProfileNull bool
wantExtensionIDs []string
}{
"authoritative fields": {
body: `{
"id":"pool-1",
"profile_id":"profile-resolved",
"extension_ids":["extension-b","extension-a"],
"browser_pool_config":{
"size":1,
"profile":{"name":"profile-selector"},
"extensions":[{"name":"extension-selector-b"},{"name":"extension-selector-a"}]
}
}`,
wantProfileID: "profile-resolved",
wantExtensionIDs: []string{"extension-b", "extension-a"},
},
"legacy ID selectors": {
body: `{
"id":"pool-1",
"browser_pool_config":{
"size":1,
"profile":{"id":"profile-legacy"},
"extensions":[{"id":"extension-b"},{"id":"extension-a"}]
}
}`,
wantProfileID: "profile-legacy",
wantExtensionIDs: []string{"extension-b", "extension-a"},
},
"no references": {
body: `{"id":"pool-1","extension_ids":[],"browser_pool_config":{"size":1}}`,
wantProfileNull: true,
wantExtensionIDs: nil,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
state, diags := flattenBrowserPool(*browserPoolFromJSON(test.body))
if diags.HasError() {
t.Fatalf("unexpected diagnostics: %v", diags)
}
if test.wantProfileNull {
if !state.ProfileID.IsNull() {
t.Fatalf("profile_id = %v, want null", state.ProfileID)
}
} else if state.ProfileID.ValueString() != test.wantProfileID {
t.Fatalf("profile_id = %q, want %q", state.ProfileID.ValueString(), test.wantProfileID)
}
assertBrowserPoolStringList(t, state.ExtensionIDs, test.wantExtensionIDs)
})
}
}

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

tests := map[string]string{
"null authoritative extensions": `{"id":"pool-1","extension_ids":null,"browser_pool_config":{"size":1}}`,
"non-list authoritative extensions": `{"id":"pool-1","extension_ids":{},"browser_pool_config":{"size":1}}`,
"empty authoritative extension ID": `{"id":"pool-1","extension_ids":[""],"browser_pool_config":{"size":1}}`,
"null authoritative profile ID": `{"id":"pool-1","profile_id":null,"extension_ids":[],"browser_pool_config":{"size":1}}`,
"non-string authoritative profile ID": `{"id":"pool-1","profile_id":1,"extension_ids":[],"browser_pool_config":{"size":1}}`,
"empty authoritative profile ID": `{"id":"pool-1","profile_id":"","extension_ids":[],"browser_pool_config":{"size":1}}`,
"legacy profile name only": `{"id":"pool-1","browser_pool_config":{"size":1,"profile":{"name":"profile-selector"}}}`,
"legacy extension name only": `{"id":"pool-1","browser_pool_config":{"size":1,"extensions":[{"name":"extension-selector"}]}}`,
}

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 TestReadBrowserPoolAllowsUnnamedPoolByID(t *testing.T) {
Expand Down Expand Up @@ -293,7 +385,7 @@ func browserPoolForTest(id, name string, size int64) *kernel.BrowserPool {
nameJSON = strconv.Quote(name)
configName = `,"name":` + strconv.Quote(name)
}
return browserPoolFromJSON(`{"id":` + strconv.Quote(id) + `,"name":` + nameJSON + `,"browser_pool_config":{"size":` + strconv.FormatInt(size, 10) + configName + `}}`)
return browserPoolFromJSON(`{"id":` + strconv.Quote(id) + `,"name":` + nameJSON + `,"extension_ids":[],"browser_pool_config":{"size":` + strconv.FormatInt(size, 10) + configName + `}}`)
}

func browserPoolFromJSON(body string) *kernel.BrowserPool {
Expand All @@ -307,16 +399,37 @@ 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,
"id": tftypes.String,
"name": tftypes.String,
"project_id": tftypes.String,
"size": tftypes.Number,
"profile_id": tftypes.String,
"extension_ids": tftypes.List{ElementType: tftypes.String},
}},
map[string]tftypes.Value{
"id": id,
"name": name,
"project_id": projectID,
"size": tftypes.NewValue(tftypes.Number, 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),
},
)
}

func assertBrowserPoolStringList(t *testing.T, got types.List, want []string) {
t.Helper()
if got.IsNull() || got.IsUnknown() {
t.Fatalf("list = %v, want %v", got, want)
}
elements := got.Elements()
if len(elements) != len(want) {
t.Fatalf("list length = %d, want %d", len(elements), len(want))
}
for index, element := range elements {
value, ok := element.(types.String)
if !ok || value.ValueString() != want[index] {
t.Fatalf("list[%d] = %v, want %q", index, element, want[index])
}
}
}