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
5 changes: 4 additions & 1 deletion .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- name: Project
package: ./internal/resources/project
project_id_required: false
- name: Extension
package: ./internal/resources/extension
project_id_required: true
# Keep the job timeout above the go test timeout so Go can report the test
# timeout before the runner stops the job. Either hard timeout can bypass
# t.Cleanup, so release operators still inspect for leaked test resources.
Expand All @@ -44,7 +47,7 @@ jobs:
env:
KERNEL_PROJECT_ID: ${{ secrets.KERNEL_PROJECT_ID }}
run: |
: "${KERNEL_PROJECT_ID:?Set the KERNEL_PROJECT_ID repository secret before running browser-pool acceptance tests.}"
: "${KERNEL_PROJECT_ID:?Set the KERNEL_PROJECT_ID repository secret before running project-scoped acceptance tests.}"

- name: Checkout
uses: actions/checkout@v6
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Provider configuration:
Resources:

- `kernel_browser_pool`
- `kernel_extension`
- `kernel_project`

Data sources:
Expand All @@ -27,6 +28,7 @@ Data sources:
Import:

- `kernel_browser_pool` by canonical browser pool ID
- `kernel_extension` by canonical extension ID, optionally qualified with its project ID
- `kernel_project` by canonical project ID

## Not Supported
Expand All @@ -39,8 +41,8 @@ The provider intentionally does not manage:
- logs, screenshots, or live view
- runtime status or standby state
- force-release or recovery operations
- API key, profile, proxy, or extension resources
- extension upload, download, delete, or Chrome Store operations
- API key, profile, proxy, or deployment resources
- extension download or Chrome Web Store download operations

## Quickstart

Expand Down Expand Up @@ -83,6 +85,8 @@ use the project-qualified form to import from a different project:
```sh
terraform import kernel_browser_pool.example <browser-pool-id>
terraform import kernel_browser_pool.example <project-id>/<browser-pool-id>
terraform import kernel_extension.example <extension-id>
terraform import kernel_extension.example <project-id>/<extension-id>
terraform import kernel_project.example <project-id>
```

Expand Down Expand Up @@ -151,12 +155,14 @@ Run the durable resource acceptance tests:

```sh
go test -count=1 -timeout=30m -v ./internal/resources/browserpool -run TestAcc
go test -count=1 -timeout=30m -v ./internal/resources/extension -run TestAcc
go test -count=1 -timeout=30m -v ./internal/resources/project -run TestAcc
```

The tests create uniquely named durable resources and register independent
cleanup. Browser-pool deletion remains `force=false`. The tests do not acquire
browsers or perform runtime recovery.
cleanup. Extension acceptance creates a small temporary Manifest V3 archive and
tests checksum-driven replacement. Browser-pool deletion remains `force=false`.
The tests do not acquire browsers or perform runtime recovery.

## Architecture

Expand Down
10 changes: 6 additions & 4 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ Use this checklist before publishing a Kernel Terraform provider version.
- Run `go vet ./...`.
- Run the complete opt-in acceptance matrix for every v1 resource and data source with real credentials before the first public release.
- Browser pools: `TF_ACC=1 KERNEL_ACC=1 KERNEL_API_KEY=... KERNEL_PROJECT_ID=... go test -count=1 -timeout=30m -v ./internal/resources/browserpool -run TestAcc`.
- Extensions: `TF_ACC=1 KERNEL_ACC=1 KERNEL_API_KEY=... KERNEL_PROJECT_ID=... go test -count=1 -timeout=30m -v ./internal/resources/extension -run TestAcc`.
- Projects: `TF_ACC=1 KERNEL_ACC=1 KERNEL_API_KEY=... go test -count=1 -timeout=30m -v ./internal/resources/project -run TestAcc`.
- The manual `Acceptance` workflow runs both packages in parallel; expand its matrix as v1 resources land and keep live tests out of normal PR CI.
- The manual `Acceptance` workflow runs all three packages in parallel; expand its matrix as v1 resources land and keep live tests out of normal PR CI.
- Process-level timeouts can bypass Go test cleanup. After an interrupted or hard-timeout run:
1. In the Kernel dashboard or durable API, find projects and browser pools named `kernel-tf-*` that were created during the failed workflow run.
1. In the Kernel dashboard or durable API, find projects, browser pools, and extensions named `kernel-tf-*` that were created during the failed workflow run.
2. Delete leaked browser pools first with `force=false`. If deletion conflicts with a lease, wait for the lease to end; do not force-release or recover the browser from Terraform cleanup.
3. Delete a leaked project only after its child resources are gone and the organization still has another active project.
4. Read each canonical resource ID and require a 404 before considering cleanup complete.
3. Delete leaked extensions after removing any durable browser-pool references to them. Do not mutate pools or running browsers implicitly.
4. Delete a leaked project only after its child resources are gone and the organization still has another active project.
5. Read each canonical resource ID and require a 404 before considering cleanup complete.
- Verify unscoped API calls send no `X-Kernel-Project-Id` header; it is sent only when a resource-level `project_id` or the provider default resolves a project.
- Confirm `terraform-registry-manifest.json` contains protocol `["6.0"]` for Terraform Plugin Framework.
- Confirm the repository license before the first public release. Do not publish a public tag until `LICENSE` exists or the release owner has explicitly documented the licensing decision.
Expand Down
42 changes: 42 additions & 0 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,48 @@ func cleanupProject(t testing.TB, client projectCleaner, id string) {
})
}

// CleanupExtension registers a cleanup that deletes the extension from
// projectID; empty means the env-configured default project.
func CleanupExtension(t testing.TB, projectID, id string) {
t.Helper()

cleanupExtension(t, ClientFromEnv(), projectID, id)
}

type extensionCleaner interface {
DefaultProjectID() string
DeleteExtension(context.Context, string, string) error
}

func cleanupExtension(t testing.TB, client extensionCleaner, projectID, id string) {
t.Helper()

if id == "" {
return
}
if !AcceptanceEnabled() {
t.Fatalf("%s must be set to clean up Kernel acceptance test resources", EnvAcceptance)
return
}
if os.Getenv(EnvAPIKey) == "" {
t.Fatalf("%s must be set to clean up Kernel acceptance test resources", EnvAPIKey)
return
}

if projectID == "" {
projectID = client.DefaultProjectID()
}

t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), cleanupTimeout)
defer cancel()

if err := client.DeleteExtension(ctx, projectID, id); err != nil && !IsNotFound(err) {
t.Errorf("cleanup Kernel extension %s: %v", id, err)
}
})
}

func ClientFromEnv() kernelclient.Clients {
return kernelclient.New(kernelclient.Config{
APIKey: os.Getenv(EnvAPIKey),
Expand Down
112 changes: 112 additions & 0 deletions internal/acctest/acctest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ type fakeProjectCleaner struct {
delete func(context.Context, string) error
}

type fakeExtensionCleaner struct {
defaultProjectID string
delete func(context.Context, string, string) error
}

func (f fakeExtensionCleaner) DefaultProjectID() string {
return f.defaultProjectID
}

func (f fakeExtensionCleaner) DeleteExtension(ctx context.Context, projectID, id string) error {
return f.delete(ctx, projectID, id)
}

func (f fakeProjectCleaner) DeleteProject(ctx context.Context, id string) error {
return f.delete(ctx, id)
}
Expand Down Expand Up @@ -343,6 +356,105 @@ func TestCleanupProject(t *testing.T) {
}
}

func TestCleanupExtension(t *testing.T) {
tests := map[string]struct {
acceptance string
apiKey string
projectID string
defaultProject string
id string
deleteErr error
wantProjectID string
wantCleanups int
wantDelete bool
wantFailure bool
}{
"empty ID is ignored": {},
"acceptance disabled": {
apiKey: "test-key",
id: "extension_123",
wantFailure: true,
},
"API key missing": {
acceptance: "1",
id: "extension_123",
wantFailure: true,
},
"default project is resolved": {
acceptance: "1",
apiKey: "test-key",
defaultProject: "project_default",
id: "extension_123",
wantProjectID: "project_default",
wantCleanups: 1,
wantDelete: true,
},
"explicit project wins": {
acceptance: "1",
apiKey: "test-key",
projectID: "project_explicit",
defaultProject: "project_default",
id: "extension_123",
deleteErr: notFoundAPIError(),
wantProjectID: "project_explicit",
wantCleanups: 1,
wantDelete: true,
},
"delete error is reported": {
acceptance: "1",
apiKey: "test-key",
projectID: "project_explicit",
id: "extension_123",
deleteErr: errors.New("connection reset"),
wantProjectID: "project_explicit",
wantCleanups: 1,
wantDelete: true,
wantFailure: true,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Setenv(EnvAcceptance, test.acceptance)
t.Setenv(EnvAPIKey, test.apiKey)

var gotID, gotProjectID string
deleteCalled := false
deadlineSet := false
recorder := &testRecorder{TB: t}
cleanupExtension(recorder, fakeExtensionCleaner{
defaultProjectID: test.defaultProject,
delete: func(ctx context.Context, projectID, id string) error {
deleteCalled = true
_, deadlineSet = ctx.Deadline()
gotProjectID = projectID
gotID = id
return test.deleteErr
},
}, test.projectID, test.id)

if got, want := len(recorder.cleanups), test.wantCleanups; got != want {
t.Fatalf("cleanupExtension registered %d cleanups, want %d", got, want)
}
if test.wantCleanups == 1 {
recorder.cleanups[0]()
}
if recorder.failed != test.wantFailure {
t.Fatalf("cleanupExtension failure = %t, want %t", recorder.failed, test.wantFailure)
}
if deleteCalled != test.wantDelete {
t.Fatalf("cleanupExtension called delete = %t, want %t", deleteCalled, test.wantDelete)
}
if deleteCalled && !deadlineSet {
t.Fatal("cleanupExtension called delete without a context deadline")
}
if test.wantDelete && (gotID != test.id || gotProjectID != test.wantProjectID) {
t.Fatalf("cleanup extension scope/id = %q/%q, want %q/%q", gotProjectID, gotID, test.wantProjectID, test.id)
}
})
}
}

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

Expand Down
Loading