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

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"

"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 extensionDeleter interface {
DeleteExtension(context.Context, string, string) error
}

func deleteExtension(ctx context.Context, client extensionDeleter, state extensionModel) 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 diags
}

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

projectID := state.ProjectID.ValueString()
if err := client.DeleteExtension(ctx, projectID, id); err != nil {
if projectscope.IsNotFound(err) {
return diags
}
if extensionDeleteInUse(err) {
diags.AddError(
"Delete Kernel Extension",
fmt.Sprintf("Kernel refused to delete extension %q because one or more browser pools reference it. Remove the extension from those durable browser pool configurations, then retry. Terraform will not mutate browser pools or running browsers implicitly.", id),
)
return diags
}
projectscope.AddError(&diags, "Delete Kernel Extension", projectID, fmt.Errorf("delete extension %q: %w", id, err))
}
return diags
}

func extensionDeleteInUse(err error) bool {
var apiError *kernel.Error
if !errors.As(err, &apiError) || apiError.StatusCode != http.StatusBadRequest {
return false
}

var body struct {
Code string `json:"code"`
Error struct {
Code string `json:"code"`
} `json:"error"`
}
if json.Unmarshal([]byte(apiError.RawJSON()), &body) != nil {
return false
}
return body.Code == "resource_in_use" || body.Error.Code == "resource_in_use"
}
112 changes: 112 additions & 0 deletions internal/resources/extension/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package extension

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

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

var _ extensionDeleter = kernelclient.Clients{}

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

func (f fakeExtensionDeleter) DeleteExtension(ctx context.Context, projectID, id string) error {
if f.delete == nil {
return errors.New("unexpected delete")
}
return f.delete(ctx, projectID, id)
}

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

var gotProjectID, gotID string
diags := deleteExtension(context.Background(), fakeExtensionDeleter{
delete: func(ctx context.Context, projectID, id string) error {
gotProjectID, gotID = projectID, id
return nil
},
}, extensionModel{ID: types.StringValue("extension_123"), ProjectID: types.StringValue("project_123")})
if diags.HasError() {
t.Fatalf("unexpected diagnostics: %v", diags)
}
if gotProjectID != "project_123" || gotID != "extension_123" {
t.Fatalf("DeleteExtension scope/id = %q/%q, want project_123/extension_123", gotProjectID, gotID)
}
}

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

diags := deleteExtension(context.Background(), fakeExtensionDeleter{
delete: func(ctx context.Context, projectID, id string) error {
return extensionAPIErrorForTest(t, http.StatusNotFound, `{"code":"not_found"}`)
},
}, extensionModel{ID: types.StringValue("extension_123"), ProjectID: types.StringNull()})
if diags.HasError() {
t.Fatalf("unexpected diagnostics: %v", diags)
}
}

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

diags := deleteExtension(context.Background(), fakeExtensionDeleter{
delete: func(ctx context.Context, projectID, id string) error {
return extensionAPIErrorForTest(t, http.StatusBadRequest, `{"code":"resource_in_use"}`)
},
}, extensionModel{ID: types.StringValue("extension_123"), ProjectID: types.StringNull()})
if !extensionDiagnosticContains(diags, "Delete Kernel Extension", "Remove the extension from those durable browser pool configurations") {
t.Fatalf("diagnostics = %v, want browser pool dependency guidance", diags)
}
}

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

tests := map[string]error{
"generic bad request": extensionAPIErrorForTest(t, http.StatusBadRequest, `{}`),
"generic not found": extensionAPIErrorForTest(t, http.StatusNotFound, `{}`),
"transport failure": errors.New("connection reset"),
}
for name, err := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
diags := deleteExtension(context.Background(), fakeExtensionDeleter{
delete: func(ctx context.Context, projectID, id string) error { return err },
}, extensionModel{ID: types.StringValue("extension_123"), ProjectID: types.StringNull()})
if !extensionDiagnosticContains(diags, "Delete Kernel Extension", "extension_123") {
t.Fatalf("diagnostics = %v, want generic delete error with ID", diags)
}
})
}
}

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

for name, state := range map[string]extensionModel{
"missing id": {ID: types.StringNull(), ProjectID: types.StringNull()},
"unknown project": {ID: types.StringValue("extension_123"), ProjectID: types.StringUnknown()},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
called := false
diags := deleteExtension(context.Background(), fakeExtensionDeleter{
delete: func(ctx context.Context, projectID, id string) error { called = true; return nil },
}, state)
if !diags.HasError() {
t.Fatal("expected invalid state diagnostic")
}
if called {
t.Fatal("DeleteExtension called for invalid state")
}
})
}
}
Loading