Skip to content

Commit 82bf203

Browse files
committed
Flatten extension metadata for resource state
1 parent 2aacf29 commit 82bf203

2 files changed

Lines changed: 226 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package extension
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
"github.com/hashicorp/terraform-plugin-framework/types"
8+
kernel "github.com/kernel/kernel-go-sdk"
9+
)
10+
11+
func flattenExtensionRead(response kernel.ExtensionGetResponse, prior extensionModel) (extensionModel, diag.Diagnostics) {
12+
var diags diag.Diagnostics
13+
14+
if !validExtensionResponseString(response.JSON.ID.Raw(), response.JSON.ID.Valid(), response.ID) {
15+
addInvalidExtensionResponseField(&diags, "id")
16+
} else if !prior.ID.IsNull() && !prior.ID.IsUnknown() && response.ID != prior.ID.ValueString() {
17+
diags.AddError(
18+
"Unexpected Kernel Extension ID",
19+
fmt.Sprintf("Kernel returned extension ID %q while reading extension %q.", response.ID, prior.ID.ValueString()),
20+
)
21+
}
22+
23+
name := types.StringNull()
24+
if extensionResponseFieldPresent(response.JSON.Name.Raw()) {
25+
if !validExtensionResponseString(response.JSON.Name.Raw(), response.JSON.Name.Valid(), response.Name) {
26+
addInvalidExtensionResponseField(&diags, "name")
27+
} else {
28+
name = types.StringValue(response.Name)
29+
}
30+
}
31+
32+
checksum := types.StringNull()
33+
if extensionResponseFieldPresent(response.JSON.Checksum.Raw()) {
34+
if !validExtensionResponseString(response.JSON.Checksum.Raw(), response.JSON.Checksum.Valid(), response.Checksum) ||
35+
!extensionChecksumPattern.MatchString(response.Checksum) {
36+
addInvalidExtensionResponseField(&diags, "checksum")
37+
} else {
38+
checksum = types.StringValue(response.Checksum)
39+
}
40+
} else if !prior.SourceSHA256.IsNull() && !prior.SourceSHA256.IsUnknown() {
41+
diags.AddError(
42+
"Missing Kernel Extension Checksum",
43+
"Kernel no longer returned a checksum for extension "+prior.ID.ValueString()+", so Terraform cannot verify the managed archive content.",
44+
)
45+
}
46+
47+
if diags.HasError() {
48+
return extensionModel{}, diags
49+
}
50+
51+
return extensionModel{
52+
ID: types.StringValue(response.ID),
53+
Name: name,
54+
ProjectID: prior.ProjectID,
55+
SourcePath: types.StringNull(),
56+
SourceSHA256: checksum,
57+
}, diags
58+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package extension
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
kernel "github.com/kernel/kernel-go-sdk"
10+
)
11+
12+
func TestFlattenExtensionReadMapsDurableState(t *testing.T) {
13+
t.Parallel()
14+
15+
checksum := strings.Repeat("a", 64)
16+
state, diags := flattenExtensionRead(
17+
extensionGetResponseForTest(t, `{
18+
"id":"extension_123",
19+
"name":"Extension",
20+
"checksum":"`+checksum+`",
21+
"created_at":"2026-01-01T00:00:00Z",
22+
"size_bytes":123,
23+
"last_used_at":"2026-01-02T00:00:00Z"
24+
}`),
25+
extensionModel{
26+
ID: types.StringValue("extension_123"),
27+
ProjectID: types.StringValue("project_123"),
28+
SourceSHA256: types.StringValue(checksum),
29+
},
30+
)
31+
if diags.HasError() {
32+
t.Fatalf("unexpected diagnostics: %v", diags)
33+
}
34+
if got, want := state.ID.ValueString(), "extension_123"; got != want {
35+
t.Fatalf("id = %q, want %q", got, want)
36+
}
37+
if got, want := state.Name.ValueString(), "Extension"; got != want {
38+
t.Fatalf("name = %q, want %q", got, want)
39+
}
40+
if !state.ProjectID.Equal(types.StringValue("project_123")) {
41+
t.Fatalf("project_id = %v, want project_123", state.ProjectID)
42+
}
43+
if !state.SourcePath.IsNull() {
44+
t.Fatalf("source_path = %v, want null write-only state", state.SourcePath)
45+
}
46+
if got, want := state.SourceSHA256.ValueString(), checksum; got != want {
47+
t.Fatalf("source_sha256 = %q, want %q", got, want)
48+
}
49+
}
50+
51+
func TestFlattenExtensionReadAllowsLegacyNullableMetadata(t *testing.T) {
52+
t.Parallel()
53+
54+
state, diags := flattenExtensionRead(
55+
extensionGetResponseForTest(t, `{"id":"extension_123","name":null}`),
56+
extensionModel{
57+
ID: types.StringValue("extension_123"),
58+
ProjectID: types.StringNull(),
59+
SourceSHA256: types.StringUnknown(),
60+
},
61+
)
62+
if diags.HasError() {
63+
t.Fatalf("unexpected diagnostics: %v", diags)
64+
}
65+
if !state.Name.IsNull() {
66+
t.Fatalf("name = %v, want null", state.Name)
67+
}
68+
if !state.ProjectID.IsNull() {
69+
t.Fatalf("project_id = %v, want null", state.ProjectID)
70+
}
71+
if !state.SourceSHA256.IsNull() {
72+
t.Fatalf("source_sha256 = %v, want null", state.SourceSHA256)
73+
}
74+
}
75+
76+
func TestFlattenExtensionReadReportsLostManagedChecksum(t *testing.T) {
77+
t.Parallel()
78+
79+
_, diags := flattenExtensionRead(
80+
extensionGetResponseForTest(t, `{"id":"extension_123","checksum":null}`),
81+
extensionModel{
82+
ID: types.StringValue("extension_123"),
83+
SourceSHA256: types.StringValue(strings.Repeat("a", 64)),
84+
},
85+
)
86+
if !extensionDiagnosticContains(diags, "Missing Kernel Extension Checksum", "cannot verify the managed archive content") {
87+
t.Fatalf("diagnostics = %v, want missing managed checksum", diags)
88+
}
89+
}
90+
91+
func TestFlattenExtensionReadSurfacesRemoteChecksumDrift(t *testing.T) {
92+
t.Parallel()
93+
94+
remoteChecksum := strings.Repeat("b", 64)
95+
state, diags := flattenExtensionRead(
96+
extensionGetResponseForTest(t, `{"id":"extension_123","checksum":"`+remoteChecksum+`"}`),
97+
extensionModel{
98+
ID: types.StringValue("extension_123"),
99+
SourceSHA256: types.StringValue(strings.Repeat("a", 64)),
100+
},
101+
)
102+
if diags.HasError() {
103+
t.Fatalf("unexpected diagnostics: %v", diags)
104+
}
105+
if got := state.SourceSHA256.ValueString(); got != remoteChecksum {
106+
t.Fatalf("source_sha256 = %q, want remote checksum %q", got, remoteChecksum)
107+
}
108+
}
109+
110+
func TestFlattenExtensionReadRejectsInvalidResponse(t *testing.T) {
111+
t.Parallel()
112+
113+
checksum := strings.Repeat("a", 64)
114+
tests := map[string]struct {
115+
body string
116+
priorID string
117+
summary string
118+
detail string
119+
}{
120+
"missing id": {
121+
body: `{"checksum":"` + checksum + `"}`,
122+
priorID: "extension_123",
123+
summary: "Invalid Kernel Extension Response",
124+
detail: "field id",
125+
},
126+
"unexpected id": {
127+
body: `{"id":"extension_other","checksum":"` + checksum + `"}`,
128+
priorID: "extension_123",
129+
summary: "Unexpected Kernel Extension ID",
130+
detail: "extension_other",
131+
},
132+
"invalid name": {
133+
body: `{"id":"extension_123","name":123,"checksum":"` + checksum + `"}`,
134+
priorID: "extension_123",
135+
summary: "Invalid Kernel Extension Response",
136+
detail: "field name",
137+
},
138+
"invalid checksum": {
139+
body: `{"id":"extension_123","checksum":"bad"}`,
140+
priorID: "extension_123",
141+
summary: "Invalid Kernel Extension Response",
142+
detail: "field checksum",
143+
},
144+
}
145+
146+
for name, test := range tests {
147+
t.Run(name, func(t *testing.T) {
148+
t.Parallel()
149+
_, diags := flattenExtensionRead(
150+
extensionGetResponseForTest(t, test.body),
151+
extensionModel{ID: types.StringValue(test.priorID), SourceSHA256: types.StringNull()},
152+
)
153+
if !extensionDiagnosticContains(diags, test.summary, test.detail) {
154+
t.Fatalf("diagnostics = %v, want %q containing %q", diags, test.summary, test.detail)
155+
}
156+
})
157+
}
158+
}
159+
160+
func extensionGetResponseForTest(t *testing.T, body string) kernel.ExtensionGetResponse {
161+
t.Helper()
162+
163+
var response kernel.ExtensionGetResponse
164+
if err := json.Unmarshal([]byte(body), &response); err != nil {
165+
t.Fatalf("unmarshal extension response: %v", err)
166+
}
167+
return response
168+
}

0 commit comments

Comments
 (0)