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
76 changes: 76 additions & 0 deletions internal/resources/extension/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package extension

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
kernel "github.com/kernel/kernel-go-sdk"
"github.com/kernel/terraform-provider-kernel/internal/projectscope"
)

type extensionReader interface {
GetExtension(context.Context, string, string) (*kernel.ExtensionGetResponse, error)
}

func readExtension(ctx context.Context, client extensionReader, state extensionModel) (extensionModel, bool, diag.Diagnostics) {
var diags diag.Diagnostics
if client == nil {
diags.AddError(
"Missing Kernel Client",
"The Kernel provider was not configured before using the extension resource.",
)
return extensionModel{}, false, diags
}

id, ok := extensionStateID(state, "read", &diags)
if !ok {
return extensionModel{}, false, diags
}
if state.ProjectID.IsUnknown() {
diags.AddAttributeError(
path.Root("project_id"),
"Unknown Kernel Project ID",
"Cannot read a Kernel extension while project_id is unknown in Terraform state.",
)
return extensionModel{}, false, diags
}

projectID := state.ProjectID.ValueString()
scope := "the API-key-bound project"
if projectID != "" {
scope = fmt.Sprintf("project %q", projectID)
}
response, err := client.GetExtension(ctx, projectID, id)
if err != nil {
if projectscope.IsNotFound(err) {
return extensionModel{}, true, diags
}
projectscope.AddError(&diags, "Read Kernel Extension", projectID, fmt.Errorf("read extension %q in %s: %w", id, scope, err))
return extensionModel{}, false, diags
}
if response == nil {
diags.AddError(
"Read Kernel Extension",
fmt.Sprintf("Kernel returned an empty response for extension %q in %s.", id, scope),
)
return extensionModel{}, false, diags
}

nextState, flattenDiags := flattenExtensionRead(*response, state)
diags.Append(flattenDiags...)
return nextState, false, diags
}

func extensionStateID(state extensionModel, operation string, diags *diag.Diagnostics) (string, bool) {
if state.ID.IsNull() || state.ID.IsUnknown() || state.ID.ValueString() == "" {
diags.AddAttributeError(
path.Root("id"),
"Missing Kernel Extension ID",
"Cannot "+operation+" a Kernel extension without a known id in Terraform state.",
)
return "", false
}
return state.ID.ValueString(), true
}
179 changes: 179 additions & 0 deletions internal/resources/extension/read_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package extension

import (
"context"
"errors"
"net/http"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-framework/types"
kernel "github.com/kernel/kernel-go-sdk"
"github.com/kernel/terraform-provider-kernel/internal/kernelclient"
)

var _ extensionReader = kernelclient.Clients{}

type fakeExtensionReader struct {
get func(context.Context, string, string) (*kernel.ExtensionGetResponse, error)
}

func (f fakeExtensionReader) GetExtension(ctx context.Context, projectID, id string) (*kernel.ExtensionGetResponse, error) {
if f.get == nil {
return nil, errors.New("unexpected get")
}
return f.get(ctx, projectID, id)
}

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

checksum := strings.Repeat("a", 64)
prior := extensionModel{
ID: types.StringValue("extension_123"),
ProjectID: types.StringValue("project_123"),
SourceSHA256: types.StringValue(checksum),
}
var gotProjectID, gotID string
next, removed, diags := readExtension(context.Background(), fakeExtensionReader{
get: func(ctx context.Context, projectID, id string) (*kernel.ExtensionGetResponse, error) {
gotProjectID, gotID = projectID, id
response := extensionGetResponseForTest(t, `{"id":"extension_123","name":"Extension","checksum":"`+checksum+`"}`)
return &response, nil
},
}, prior)
if diags.HasError() {
t.Fatalf("unexpected diagnostics: %v", diags)
}
if removed {
t.Fatal("removed = true, want false")
}
if gotProjectID != "project_123" || gotID != "extension_123" {
t.Fatalf("GetExtension scope/id = %q/%q, want project_123/extension_123", gotProjectID, gotID)
}
if got, want := next.Name.ValueString(), "Extension"; got != want {
t.Fatalf("name = %q, want %q", got, want)
}
if !next.ProjectID.Equal(prior.ProjectID) {
t.Fatalf("project_id = %v, want %v", next.ProjectID, prior.ProjectID)
}
}

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

tests := map[string]struct {
projectID types.String
err error
wantRemoved bool
wantSummary string
wantDetail string
}{
"extension not found": {
err: extensionAPIErrorForTest(t, http.StatusNotFound, `{"code":"not_found"}`),
wantRemoved: true,
},
"project not found": {
projectID: types.StringValue("project_123"),
err: extensionAPIErrorForTest(t, http.StatusNotFound, `{"code":"project_not_found"}`),
wantSummary: "Kernel Project Not Found",
wantDetail: `extension_123`,
},
"uncoded not found": {
err: extensionAPIErrorForTest(t, http.StatusNotFound, `{}`),
wantSummary: "Read Kernel Extension",
wantDetail: "the API-key-bound project",
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, removed, diags := readExtension(context.Background(), fakeExtensionReader{
get: func(ctx context.Context, projectID, id string) (*kernel.ExtensionGetResponse, error) {
return nil, test.err
},
}, extensionModel{ID: types.StringValue("extension_123"), ProjectID: test.projectID})
if removed != test.wantRemoved {
t.Fatalf("removed = %t, want %t", removed, test.wantRemoved)
}
if test.wantSummary == "" {
if diags.HasError() {
t.Fatalf("unexpected diagnostics: %v", diags)
}
} else if !extensionDiagnosticContains(diags, test.wantSummary, test.wantDetail) {
t.Fatalf("diagnostics = %v, want %q", diags, test.wantSummary)
}
})
}
}

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

tests := map[string]extensionModel{
"missing id": {ID: types.StringNull(), ProjectID: types.StringNull()},
"unknown id": {ID: types.StringUnknown(), ProjectID: types.StringNull()},
"empty id": {ID: types.StringValue(""), ProjectID: types.StringNull()},
"unknown project": {ID: types.StringValue("extension_123"), ProjectID: types.StringUnknown()},
}

for name, state := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
called := false
_, removed, diags := readExtension(context.Background(), fakeExtensionReader{
get: func(ctx context.Context, projectID, id string) (*kernel.ExtensionGetResponse, error) {
called = true
return nil, nil
},
}, state)
if !diags.HasError() {
t.Fatal("expected invalid-state diagnostic")
}
if removed {
t.Fatal("removed = true, want false")
}
if called {
t.Fatal("GetExtension called for invalid state")
}
})
}
}

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

state := extensionModel{ID: types.StringValue("extension_123"), ProjectID: types.StringNull()}
tests := map[string]struct {
client extensionReader
summary string
detail string
}{
"missing client": {
summary: "Missing Kernel Client",
},
"empty response": {
client: fakeExtensionReader{
get: func(ctx context.Context, projectID, id string) (*kernel.ExtensionGetResponse, error) {
return nil, nil
},
},
summary: "Read Kernel Extension",
detail: `extension_123`,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, removed, diags := readExtension(context.Background(), test.client, state)
if removed {
t.Fatal("removed = true, want false")
}
if !extensionDiagnosticContains(diags, test.summary, test.detail) {
t.Fatalf("diagnostics = %v, want %q", diags, test.summary)
}
})
}
}