Skip to content

Commit a741065

Browse files
committed
Register the durable extension resource
Assemble the reviewed extension lifecycle helpers behind a Terraform Plugin Framework resource, require local archive inputs only for create or replacement, and publish generated schema documentation. This makes the durable extension surface usable while keeping runtime operations out of Terraform.
1 parent af1b09e commit a741065

7 files changed

Lines changed: 481 additions & 5 deletions

File tree

docs/resources/extension.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "kernel_extension Resource - Kernel"
4+
subcategory: ""
5+
description: |-
6+
Kernel uploaded extension durable configuration.
7+
---
8+
9+
# kernel_extension (Resource)
10+
11+
Kernel uploaded extension durable configuration.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Optional
19+
20+
> **NOTE**: [Write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments) are supported in Terraform 1.11 and later.
21+
22+
- `name` (String) Optional extension name. Must be unique within the project. Adding or changing a configured name replaces the extension; omitting it preserves the remote name because the API cannot clear a name.
23+
- `project_id` (String) Project this extension belongs to. Defaults to the provider `project_id` when unset; when neither is set, the API key's project binding determines the project. Adding or changing it replaces the extension.
24+
- `source_path` (String, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Local path to the extension ZIP. Required when creating or replacing the extension and never stored in Terraform plan or state artifacts. Requires Terraform 1.11 or later.
25+
- `source_sha256` (String) Lowercase hexadecimal SHA-256 checksum of the exact extension ZIP bytes. Configure with `filesha256(source_path)`. Adding or changing it replaces the extension.
26+
27+
### Read-Only
28+
29+
- `id` (String) Unique extension identifier.

internal/provider/provider.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77
"github.com/hashicorp/terraform-plugin-framework/provider"
88
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
99
"github.com/hashicorp/terraform-plugin-framework/resource"
10-
"github.com/kernel/terraform-provider-kernel/internal/datasources/extension"
10+
extensiondatasource "github.com/kernel/terraform-provider-kernel/internal/datasources/extension"
1111
"github.com/kernel/terraform-provider-kernel/internal/datasources/profile"
1212
projectdatasource "github.com/kernel/terraform-provider-kernel/internal/datasources/project"
1313
"github.com/kernel/terraform-provider-kernel/internal/datasources/proxy"
1414
"github.com/kernel/terraform-provider-kernel/internal/kernelclient"
1515
"github.com/kernel/terraform-provider-kernel/internal/resources/browserpool"
16+
extensionresource "github.com/kernel/terraform-provider-kernel/internal/resources/extension"
1617
projectresource "github.com/kernel/terraform-provider-kernel/internal/resources/project"
1718
)
1819

@@ -82,6 +83,7 @@ func (p *kernelProvider) Configure(ctx context.Context, req provider.ConfigureRe
8283
func (p *kernelProvider) Resources(ctx context.Context) []func() resource.Resource {
8384
return []func() resource.Resource{
8485
browserpool.NewResource,
86+
extensionresource.NewResource,
8587
projectresource.NewResource,
8688
}
8789
}
@@ -91,6 +93,6 @@ func (p *kernelProvider) DataSources(ctx context.Context) []func() datasource.Da
9193
projectdatasource.NewDataSource,
9294
profile.NewDataSource,
9395
proxy.NewDataSource,
94-
extension.NewDataSource,
96+
extensiondatasource.NewDataSource,
9597
}
9698
}

internal/provider/provider_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ func TestProviderRegistersResources(t *testing.T) {
3434
p := provider.New("test")()
3535

3636
resources := p.Resources(context.Background())
37-
if len(resources) != 2 {
38-
t.Fatalf("Resources length = %d, want 2", len(resources))
37+
if len(resources) != 3 {
38+
t.Fatalf("Resources length = %d, want 3", len(resources))
3939
}
4040

4141
got := make(map[string]bool, len(resources))
@@ -49,7 +49,7 @@ func TestProviderRegistersResources(t *testing.T) {
4949
got[resp.TypeName] = true
5050
}
5151

52-
for _, want := range []string{"kernel_browser_pool", "kernel_project"} {
52+
for _, want := range []string{"kernel_browser_pool", "kernel_extension", "kernel_project"} {
5353
if !got[want] {
5454
t.Fatalf("missing resource %s; got %v", want, got)
5555
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package extension
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/resource"
8+
"github.com/kernel/terraform-provider-kernel/internal/kernelclient"
9+
)
10+
11+
var _ extensionClient = kernelclient.Clients{}
12+
13+
func TestExtensionResourceMetadataAndSchema(t *testing.T) {
14+
t.Parallel()
15+
16+
r := NewResource()
17+
var metadata resource.MetadataResponse
18+
r.Metadata(context.Background(), resource.MetadataRequest{ProviderTypeName: "kernel"}, &metadata)
19+
if metadata.TypeName != "kernel_extension" {
20+
t.Fatalf("type name = %q, want kernel_extension", metadata.TypeName)
21+
}
22+
23+
var schema resource.SchemaResponse
24+
r.Schema(context.Background(), resource.SchemaRequest{}, &schema)
25+
for _, name := range []string{"id", "name", "project_id", "source_path", "source_sha256"} {
26+
if _, ok := schema.Schema.Attributes[name]; !ok {
27+
t.Fatalf("extension schema missing %s attribute", name)
28+
}
29+
}
30+
}
31+
32+
func TestExtensionResourceConfigure(t *testing.T) {
33+
t.Parallel()
34+
35+
t.Run("durable client", func(t *testing.T) {
36+
t.Parallel()
37+
r := &extensionResource{}
38+
var resp resource.ConfigureResponse
39+
r.Configure(context.Background(), resource.ConfigureRequest{ProviderData: kernelclient.Clients{}}, &resp)
40+
if resp.Diagnostics.HasError() {
41+
t.Fatalf("unexpected diagnostics: %v", resp.Diagnostics)
42+
}
43+
if r.client == nil {
44+
t.Fatal("extension client was not configured")
45+
}
46+
})
47+
48+
t.Run("unexpected provider data", func(t *testing.T) {
49+
t.Parallel()
50+
r := &extensionResource{}
51+
var resp resource.ConfigureResponse
52+
r.Configure(context.Background(), resource.ConfigureRequest{ProviderData: "not a client"}, &resp)
53+
if len(resp.Diagnostics) != 1 || resp.Diagnostics[0].Summary() != "Unexpected Kernel Client Type" {
54+
t.Fatalf("diagnostics = %v, want Unexpected Kernel Client Type", resp.Diagnostics)
55+
}
56+
})
57+
58+
t.Run("nil provider data", func(t *testing.T) {
59+
t.Parallel()
60+
r := &extensionResource{}
61+
var resp resource.ConfigureResponse
62+
r.Configure(context.Background(), resource.ConfigureRequest{}, &resp)
63+
if resp.Diagnostics.HasError() {
64+
t.Fatalf("unexpected diagnostics: %v", resp.Diagnostics)
65+
}
66+
if r.client != nil {
67+
t.Fatal("nil provider data configured a client")
68+
}
69+
})
70+
}
71+
72+
func TestExtensionResourceRejectsUpdate(t *testing.T) {
73+
t.Parallel()
74+
75+
r := &extensionResource{}
76+
var resp resource.UpdateResponse
77+
r.Update(context.Background(), resource.UpdateRequest{}, &resp)
78+
79+
if len(resp.Diagnostics) != 1 || resp.Diagnostics[0].Summary() != "Unexpected Kernel Extension Update" {
80+
t.Fatalf("diagnostics = %v, want Unexpected Kernel Extension Update", resp.Diagnostics)
81+
}
82+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package extension
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
"github.com/hashicorp/terraform-plugin-framework/resource"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
func modifyExtensionPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
12+
if req.Plan.Raw.IsNull() {
13+
return
14+
}
15+
16+
var config extensionModel
17+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
18+
var plan extensionModel
19+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
20+
if resp.Diagnostics.HasError() {
21+
return
22+
}
23+
24+
requiresArchive := req.State.Raw.IsNull()
25+
if !requiresArchive {
26+
var state extensionModel
27+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
28+
if resp.Diagnostics.HasError() {
29+
return
30+
}
31+
requiresArchive = extensionReplacementPlanned(state, plan)
32+
}
33+
if !requiresArchive {
34+
return
35+
}
36+
37+
if config.SourcePath.IsNull() {
38+
resp.Diagnostics.AddAttributeError(
39+
path.Root("source_path"),
40+
"Missing Extension Source Path",
41+
"source_path must be configured when creating or replacing a Kernel extension.",
42+
)
43+
}
44+
if config.SourceSHA256.IsNull() {
45+
resp.Diagnostics.AddAttributeError(
46+
path.Root("source_sha256"),
47+
"Missing Extension Source Checksum",
48+
"source_sha256 must be configured when creating or replacing a Kernel extension. Use filesha256(source_path) to track the exact archive bytes.",
49+
)
50+
}
51+
}
52+
53+
func extensionReplacementPlanned(state, plan extensionModel) bool {
54+
return extensionStringChanged(state.Name, plan.Name) ||
55+
extensionStringChanged(state.ProjectID, plan.ProjectID) ||
56+
extensionStringChanged(state.SourceSHA256, plan.SourceSHA256)
57+
}
58+
59+
func extensionStringChanged(state, plan types.String) bool {
60+
return !state.Equal(plan)
61+
}

0 commit comments

Comments
 (0)