diff --git a/docs/data-sources/apikeys.md b/docs/data-sources/apikeys.md
index d0d8ddf..6faac0d 100644
--- a/docs/data-sources/apikeys.md
+++ b/docs/data-sources/apikeys.md
@@ -28,4 +28,14 @@ data "supabase_apikeys" "production" {
### Read-Only
- `anon_key` (String, Sensitive) Anonymous API key for the project
+- `publishable_key` (String, Sensitive) Publishable API key for the project
+- `secret_keys` (Attributes List, Sensitive) List of secret API keys for the project (see [below for nested schema](#nestedatt--secret_keys))
- `service_role_key` (String, Sensitive) Service role API key for the project
+
+
+### Nested Schema for `secret_keys`
+
+Read-Only:
+
+- `api_key` (String, Sensitive) The secret API key value
+- `name` (String) Name of the secret key
diff --git a/docs/resources/apikey.md b/docs/resources/apikey.md
new file mode 100644
index 0000000..c00f05e
--- /dev/null
+++ b/docs/resources/apikey.md
@@ -0,0 +1,57 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "supabase_apikey Resource - terraform-provider-supabase"
+subcategory: ""
+description: |-
+ API Key resource
+---
+
+# supabase_apikey (Resource)
+
+API Key resource
+
+## Example Usage
+
+```terraform
+resource "supabase_apikey" "new" {
+ project_ref = "mayuaycdtijbctgqbycg"
+ name = "test"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `name` (String) Name of the API key
+- `project_ref` (String) Project reference ID
+
+### Optional
+
+- `description` (String) Description of the API key
+
+### Read-Only
+
+- `api_key` (String, Sensitive) API key
+- `id` (String) API key identifier
+- `secret_jwt_template` (Attributes) Secret JWT template (see [below for nested schema](#nestedatt--secret_jwt_template))
+- `type` (String) Type of the API key
+
+
+### Nested Schema for `secret_jwt_template`
+
+Read-Only:
+
+- `role` (String) Role of the secret JWT template
+
+## Import
+
+Import is supported using the following syntax:
+
+The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
+
+```shell
+# The ID is the project reference and a unique identifier of the key separated by '/'
+terraform import supabase_apikey.example /
+```
diff --git a/docs/schema.json b/docs/schema.json
index 637976e..658dae3 100644
--- a/docs/schema.json
+++ b/docs/schema.json
@@ -24,6 +24,68 @@
}
},
"resource_schemas": {
+ "supabase_apikey": {
+ "version": 0,
+ "block": {
+ "attributes": {
+ "api_key": {
+ "type": "string",
+ "description": "API key",
+ "description_kind": "markdown",
+ "computed": true,
+ "sensitive": true
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the API key",
+ "description_kind": "markdown",
+ "optional": true
+ },
+ "id": {
+ "type": "string",
+ "description": "API key identifier",
+ "description_kind": "markdown",
+ "computed": true
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the API key",
+ "description_kind": "markdown",
+ "required": true
+ },
+ "project_ref": {
+ "type": "string",
+ "description": "Project reference ID",
+ "description_kind": "markdown",
+ "required": true
+ },
+ "secret_jwt_template": {
+ "nested_type": {
+ "attributes": {
+ "role": {
+ "type": "string",
+ "description": "Role of the secret JWT template",
+ "description_kind": "markdown",
+ "computed": true
+ }
+ },
+ "nesting_mode": "single"
+ },
+ "description": "Secret JWT template",
+ "description_kind": "markdown",
+ "computed": true
+ },
+ "type": {
+ "type": "string",
+ "description": "Type of the API key",
+ "description_kind": "markdown",
+ "computed": true
+ }
+ },
+ "description": "API Key resource",
+ "description_kind": "markdown"
+ }
+ },
"supabase_branch": {
"version": 0,
"block": {
@@ -240,6 +302,37 @@
"description_kind": "markdown",
"required": true
},
+ "publishable_key": {
+ "type": "string",
+ "description": "Publishable API key for the project",
+ "description_kind": "markdown",
+ "computed": true,
+ "sensitive": true
+ },
+ "secret_keys": {
+ "nested_type": {
+ "attributes": {
+ "api_key": {
+ "type": "string",
+ "description": "The secret API key value",
+ "description_kind": "markdown",
+ "computed": true,
+ "sensitive": true
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the secret key",
+ "description_kind": "markdown",
+ "computed": true
+ }
+ },
+ "nesting_mode": "list"
+ },
+ "description": "List of secret API keys for the project",
+ "description_kind": "markdown",
+ "computed": true,
+ "sensitive": true
+ },
"service_role_key": {
"type": "string",
"description": "Service role API key for the project",
diff --git a/examples/examples.go b/examples/examples.go
index 8e37835..adbf701 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -9,6 +9,8 @@ var (
ProjectResourceConfig string
//go:embed resources/supabase_branch/resource.tf
BranchResourceConfig string
+ //go:embed resources/supabase_apikey/resource.tf
+ ApiKeyResourceConfig string
//go:embed data-sources/supabase_branch/data-source.tf
BranchDataSourceConfig string
//go:embed data-sources/supabase_pooler/data-source.tf
diff --git a/examples/resources/supabase_apikey/import.sh b/examples/resources/supabase_apikey/import.sh
new file mode 100644
index 0000000..548b673
--- /dev/null
+++ b/examples/resources/supabase_apikey/import.sh
@@ -0,0 +1,2 @@
+# The ID is the project reference and a unique identifier of the key separated by '/'
+terraform import supabase_apikey.example /
\ No newline at end of file
diff --git a/examples/resources/supabase_apikey/resource.tf b/examples/resources/supabase_apikey/resource.tf
new file mode 100644
index 0000000..86922d8
--- /dev/null
+++ b/examples/resources/supabase_apikey/resource.tf
@@ -0,0 +1,4 @@
+resource "supabase_apikey" "new" {
+ project_ref = "mayuaycdtijbctgqbycg"
+ name = "test"
+}
diff --git a/internal/provider/apikey_resource.go b/internal/provider/apikey_resource.go
new file mode 100644
index 0000000..b73d035
--- /dev/null
+++ b/internal/provider/apikey_resource.go
@@ -0,0 +1,422 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/oapi-codegen/nullable"
+ "github.com/supabase/cli/pkg/api"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.Resource = &APIKeyResource{}
+var _ resource.ResourceWithImportState = &APIKeyResource{}
+
+func NewApiKeyResource() resource.Resource {
+ return &APIKeyResource{}
+}
+
+// APIKeysDataSource defines the data source implementation.
+type APIKeyResource struct {
+ client *api.ClientWithResponses
+}
+
+var secretJwtTemplateAttrTypes = map[string]attr.Type{
+ "role": types.StringType,
+}
+
+type ApiKeyDatabaseModel struct {
+ Id types.String `tfsdk:"id"`
+ ProjectRef types.String `tfsdk:"project_ref"`
+ ApiKey types.String `tfsdk:"api_key"`
+ SecretJwtTemplate types.Object `tfsdk:"secret_jwt_template"`
+ Name types.String `tfsdk:"name"`
+ Type types.String `tfsdk:"type"`
+ Description types.String `tfsdk:"description"`
+}
+
+func (m ApiKeyDatabaseModel) AttributeTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "role": types.StringType,
+ }
+}
+
+// APIKeysDataSourceModel describes the data source data model.
+type ApiKeyResourceModel struct {
+ ProjectRef types.String `tfsdk:"project_ref"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ Type types.String `tfsdk:"type"`
+ ApiKey types.String `tfsdk:"api_key"`
+ SecretJwtTemplate types.Object `tfsdk:"secret_jwt_template"`
+ Id types.String `tfsdk:"id"`
+}
+
+func (d *APIKeyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_apikey"
+}
+
+func (d *APIKeyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "API Key resource",
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "API key identifier",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "project_ref": schema.StringAttribute{
+ MarkdownDescription: "Project reference ID",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Name of the API key",
+ Required: true,
+ },
+ "description": schema.StringAttribute{
+ MarkdownDescription: "Description of the API key",
+ Optional: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "Type of the API key",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "api_key": schema.StringAttribute{
+ MarkdownDescription: "API key",
+ Computed: true,
+ Sensitive: true,
+ },
+ "secret_jwt_template": schema.SingleNestedAttribute{
+ MarkdownDescription: "Secret JWT template",
+ Computed: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ Attributes: map[string]schema.Attribute{
+ "role": schema.StringAttribute{
+ MarkdownDescription: "Role of the secret JWT template",
+ Computed: true,
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *APIKeyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ // Prevent panic if the provider has not been configured.
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(*api.ClientWithResponses)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *api.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = client
+}
+
+func (r *APIKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data ApiKeyResourceModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(createApiKey(ctx, &data, r.client)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Write logs using the tflog package
+ // Documentation: https://terraform.io/plugin/log
+ tflog.Trace(ctx, "created a resource")
+
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *APIKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data ApiKeyResourceModel
+
+ // Read Terraform configuration data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(readApiKey(ctx, &data, r.client)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *APIKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data ApiKeyResourceModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(updateApiKey(ctx, &data, r.client)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *APIKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ parts := strings.Split(req.ID, "/")
+ if len(parts) != 2 {
+ resp.Diagnostics.AddError(
+ "Unexpected Import Identifier",
+ `Expected import identifier in the format "project_ref/api_key_id".`,
+ )
+ return
+ }
+
+ projectRef := strings.TrimSpace(parts[0])
+ apiKeyID := strings.TrimSpace(parts[1])
+ if projectRef == "" || apiKeyID == "" {
+ resp.Diagnostics.AddError(
+ "Unexpected Import Identifier",
+ "Both project_ref and api_key_id must be provided when importing. Example: myprojectref/3603c575-...",
+ )
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_ref"), types.StringValue(projectRef))...)
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.StringValue(apiKeyID))...)
+}
+
+func (r *APIKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data ApiKeyResourceModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(deleteApiKey(ctx, &data, r.client)...)
+}
+
+func readApiKey(ctx context.Context, state *ApiKeyResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
+ return readApiKeyDatabase(ctx, state, client)
+}
+
+func readApiKeyDatabase(ctx context.Context, state *ApiKeyResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
+ httpResp, err := client.V1GetProjectApiKeyWithResponse(ctx, state.ProjectRef.ValueString(), uuid.MustParse(state.Id.ValueString()), &api.V1GetProjectApiKeyParams{Reveal: Ptr(true)})
+ if err != nil {
+ msg := fmt.Sprintf("Unable to read apiKey database, got error: %s", err)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ if httpResp.JSON200 == nil {
+ msg := fmt.Sprintf("Unable to read apiKey database, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+
+ idValue := NullableToString(httpResp.JSON200.Id)
+ apiKeyValue := NullableToString(httpResp.JSON200.ApiKey)
+ typeValue := NullableToString(httpResp.JSON200.Type)
+ descriptionValue := NullableToString(httpResp.JSON200.Description)
+
+ database := ApiKeyDatabaseModel{
+ Id: idValue,
+ ApiKey: apiKeyValue,
+ Name: types.StringValue(httpResp.JSON200.Name),
+ Type: typeValue,
+ Description: descriptionValue,
+ }
+
+ var secretJwtTemplate types.Object
+ if httpResp.JSON200.SecretJwtTemplate.IsSpecified() && !httpResp.JSON200.SecretJwtTemplate.IsNull() {
+ templateMap := httpResp.JSON200.SecretJwtTemplate.MustGet()
+ roleValue := ""
+ if role, ok := templateMap["role"].(string); ok {
+ roleValue = role
+ }
+ obj, diags := types.ObjectValue(secretJwtTemplateAttrTypes, map[string]attr.Value{
+ "role": types.StringValue(roleValue),
+ })
+ if diags.HasError() {
+ return diags
+ }
+ secretJwtTemplate = obj
+ } else {
+ obj, diags := types.ObjectValue(secretJwtTemplateAttrTypes, map[string]attr.Value{
+ "role": types.StringNull(),
+ })
+ if diags.HasError() {
+ return diags
+ }
+ secretJwtTemplate = obj
+ }
+
+ database.SecretJwtTemplate = secretJwtTemplate
+ state.Id = database.Id
+ state.ApiKey = database.ApiKey
+ state.Name = database.Name
+ state.Type = database.Type
+ state.Description = database.Description
+ state.SecretJwtTemplate = database.SecretJwtTemplate
+ return nil
+}
+
+func createApiKey(ctx context.Context, plan *ApiKeyResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
+ reveal := Ptr(true)
+ resp, err := client.V1GetProjectApiKeysWithResponse(ctx, plan.ProjectRef.ValueString(), &api.V1GetProjectApiKeysParams{Reveal: reveal})
+ if err != nil {
+ msg := fmt.Sprintf("Unable to read apiKeys, got error: %s", err)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+
+ // 1. Check if default publishable key exist, create it if it doesn't
+ hasDefaultPublishable := false
+
+ if resp.JSON200 != nil {
+ for _, key := range *resp.JSON200 {
+ if key.Name == "default" {
+ if key.Type.IsSpecified() && !key.Type.IsNull() {
+ keyType := key.Type.MustGet()
+ if keyType == api.ApiKeyResponseTypePublishable {
+ hasDefaultPublishable = true
+ }
+ }
+ }
+ }
+ }
+
+ if !hasDefaultPublishable {
+ httpRespDefaultPublishable, errDefaultPublishable := client.V1CreateProjectApiKeyWithResponse(ctx, plan.ProjectRef.ValueString(), &api.V1CreateProjectApiKeyParams{Reveal: reveal}, api.CreateApiKeyBody{
+ Name: "default",
+ Type: api.CreateApiKeyBodyTypePublishable,
+ Description: nullable.Nullable[string]{},
+ SecretJwtTemplate: nullable.Nullable[map[string]interface{}]{},
+ })
+ if errDefaultPublishable != nil {
+ msg := fmt.Sprintf("Unable to create default publishable apiKey, got error: %s", errDefaultPublishable)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ if httpRespDefaultPublishable.JSON201 == nil {
+ msg := fmt.Sprintf("Unable to create default publishable apiKey, got status %d: %s", httpRespDefaultPublishable.StatusCode(), httpRespDefaultPublishable.Body)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ }
+
+ // 2. Create apiKey
+ httpResp, err := client.V1CreateProjectApiKeyWithResponse(ctx, plan.ProjectRef.ValueString(), &api.V1CreateProjectApiKeyParams{Reveal: reveal}, api.CreateApiKeyBody{
+ Name: plan.Name.ValueString(),
+ Type: api.CreateApiKeyBodyTypeSecret,
+ Description: nullable.Nullable[string]{},
+ SecretJwtTemplate: nullable.NewNullableWithValue(map[string]interface{}{"role": "service_role"}),
+ })
+
+ if err != nil {
+ msg := fmt.Sprintf("Unable to create apiKey, got error: %s", err)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ if httpResp.JSON201 == nil {
+ msg := fmt.Sprintf("Unable to create apiKey, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+
+ // Update computed fields from creation response
+ plan.Id = NullableToString(httpResp.JSON201.Id)
+ plan.ApiKey = NullableToString(httpResp.JSON201.ApiKey)
+ plan.Type = NullableToString(httpResp.JSON201.Type)
+
+ obj, diags := types.ObjectValue(secretJwtTemplateAttrTypes, map[string]attr.Value{
+ "role": types.StringValue("service_role"),
+ })
+ if diags.HasError() {
+ return diags
+ }
+ plan.SecretJwtTemplate = obj
+
+ return readApiKeyDatabase(ctx, plan, client)
+}
+
+func updateApiKey(ctx context.Context, plan *ApiKeyResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
+ var secretJwtTemplate nullable.Nullable[map[string]interface{}]
+ if plan.Type.ValueString() == string(api.ApiKeyResponseTypeSecret) {
+ secretJwtTemplate = nullable.NewNullableWithValue(map[string]interface{}{"role": "service_role"})
+ } else {
+ secretJwtTemplate = nullable.Nullable[map[string]interface{}]{}
+ }
+
+ var description nullable.Nullable[string]
+ if plan.Description.IsNull() || plan.Description.IsUnknown() {
+ description = nullable.Nullable[string]{}
+ } else {
+ description = nullable.NewNullableWithValue(plan.Description.ValueString())
+ }
+
+ httpResp, err := client.V1UpdateProjectApiKeyWithResponse(ctx, plan.ProjectRef.ValueString(), uuid.MustParse(plan.Id.ValueString()), &api.V1UpdateProjectApiKeyParams{Reveal: Ptr(true)}, api.UpdateApiKeyBody{
+ Name: plan.Name.ValueStringPointer(),
+ Description: description,
+ SecretJwtTemplate: secretJwtTemplate,
+ })
+
+ if err != nil {
+ msg := fmt.Sprintf("Unable to update apiKey, got error: %s", err)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ if httpResp.JSON200 == nil {
+ msg := fmt.Sprintf("Unable to update apiKey, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+
+ plan.Description = NullableToString(httpResp.JSON200.Description)
+
+ return readApiKeyDatabase(ctx, plan, client)
+}
+
+func deleteApiKey(ctx context.Context, state *ApiKeyResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
+ httpResp, err := client.V1DeleteProjectApiKeyWithResponse(ctx, state.ProjectRef.ValueString(), uuid.MustParse(state.Id.ValueString()), &api.V1DeleteProjectApiKeyParams{Reveal: Ptr(true)})
+ if err != nil {
+ msg := fmt.Sprintf("Unable to delete apiKey, got error: %s", err)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ if httpResp.StatusCode() != http.StatusOK {
+ msg := fmt.Sprintf("Unable to delete apiKey, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
+ return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
+ }
+ return nil
+}
diff --git a/internal/provider/apikey_resource_test.go b/internal/provider/apikey_resource_test.go
new file mode 100644
index 0000000..5b20ffe
--- /dev/null
+++ b/internal/provider/apikey_resource_test.go
@@ -0,0 +1,116 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package provider
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/oapi-codegen/nullable"
+ "github.com/supabase/cli/pkg/api"
+ "github.com/supabase/terraform-provider-supabase/examples"
+ "gopkg.in/h2non/gock.v1"
+)
+
+const testProjectRef = "mayuaycdtijbctgqbycg" //nolint:gosec
+
+func TestAccApiKeyResource(t *testing.T) {
+ // Setup mock api
+ defer gock.OffAll()
+ // Step 1: create
+ testApiKeyUUID := uuid.New()
+ apiKeysEndpoint := fmt.Sprintf("/v1/projects/%s/api-keys", testProjectRef)
+ apiKeyEndpoint := fmt.Sprintf("%s/%s", apiKeysEndpoint, testApiKeyUUID.String())
+ gock.New("https://api.supabase.com").
+ Get(apiKeysEndpoint).
+ Reply(http.StatusOK).
+ JSON([]api.ApiKeyResponse{
+ {
+ Name: "anon",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeLegacy),
+ ApiKey: nullable.NewNullableWithValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.anon"),
+ },
+ {
+ Name: "service_role",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeLegacy),
+ ApiKey: nullable.NewNullableWithValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.service_role"),
+ },
+ })
+ gock.New("https://api.supabase.com").
+ Post(apiKeysEndpoint).
+ Reply(http.StatusCreated).
+ JSON(api.ApiKeyResponse{
+ Id: nullable.NewNullableWithValue(uuid.New().String()),
+ Name: "default",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypePublishable),
+ ApiKey: nullable.NewNullableWithValue("sb_publishable_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ })
+ gock.New("https://api.supabase.com").
+ Post(apiKeysEndpoint).
+ Reply(http.StatusCreated).
+ JSON(api.ApiKeyResponse{
+ Id: nullable.NewNullableWithValue(testApiKeyUUID.String()),
+ Name: "test",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeSecret),
+ ApiKey: nullable.NewNullableWithValue("sb_secret_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ })
+ gock.New("https://api.supabase.com").
+ Get(apiKeyEndpoint).
+ Persist().
+ Reply(http.StatusOK).
+ JSON(api.ApiKeyResponse{
+ Id: nullable.NewNullableWithValue(testApiKeyUUID.String()),
+ Name: "test",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeSecret),
+ ApiKey: nullable.NewNullableWithValue("sb_secret_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ SecretJwtTemplate: nullable.NewNullableWithValue(map[string]interface{}{
+ "role": "service_role",
+ }),
+ })
+ gock.New("https://api.supabase.com").
+ Delete(apiKeyEndpoint).
+ Reply(http.StatusOK)
+
+ // Run test
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: examples.ApiKeyResourceConfig,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("supabase_apikey.new", "id", testApiKeyUUID.String()),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "supabase_apikey.new",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateVerifyIgnore: []string{"name", "project_ref"},
+ ImportStateId: fmt.Sprintf("%s/%s", testProjectRef, testApiKeyUUID.String()),
+ },
+ // Update and Read testing
+ {
+ Config: testAccApikeyResourceConfig,
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("supabase_apikey.new", "name", "test"),
+ resource.TestCheckResourceAttr("supabase_apikey.new", "project_ref", testProjectRef),
+ ),
+ },
+ // Delete testing automatically occurs in TestCase
+ },
+ })
+}
+
+const testAccApikeyResourceConfig = `
+resource "supabase_apikey" "new" {
+ project_ref = "` + testProjectRef + `"
+ name = "test"
+}
+`
diff --git a/internal/provider/apikeys_data_source.go b/internal/provider/apikeys_data_source.go
index d010a2e..f6ede2c 100644
--- a/internal/provider/apikeys_data_source.go
+++ b/internal/provider/apikeys_data_source.go
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -31,6 +32,8 @@ type APIKeysDataSourceModel struct {
ProjectRef types.String `tfsdk:"project_ref"`
AnonKey types.String `tfsdk:"anon_key"`
ServiceRoleKey types.String `tfsdk:"service_role_key"`
+ PublishableKey types.String `tfsdk:"publishable_key"`
+ SecretKeys types.List `tfsdk:"secret_keys"`
}
func (d *APIKeysDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
@@ -56,6 +59,29 @@ func (d *APIKeysDataSource) Schema(ctx context.Context, req datasource.SchemaReq
Computed: true,
Sensitive: true,
},
+ "publishable_key": schema.StringAttribute{
+ MarkdownDescription: "Publishable API key for the project",
+ Computed: true,
+ Sensitive: true,
+ },
+ "secret_keys": schema.ListNestedAttribute{
+ MarkdownDescription: "List of secret API keys for the project",
+ Computed: true,
+ Sensitive: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Name of the secret key",
+ Computed: true,
+ },
+ "api_key": schema.StringAttribute{
+ MarkdownDescription: "The secret API key value",
+ Computed: true,
+ Sensitive: true,
+ },
+ },
+ },
+ },
},
}
}
@@ -87,7 +113,7 @@ func (d *APIKeysDataSource) Read(ctx context.Context, req datasource.ReadRequest
return
}
- httpResp, err := d.client.V1GetProjectApiKeysWithResponse(ctx, data.ProjectRef.ValueString(), &api.V1GetProjectApiKeysParams{})
+ httpResp, err := d.client.V1GetProjectApiKeysWithResponse(ctx, data.ProjectRef.ValueString(), &api.V1GetProjectApiKeysParams{Reveal: Ptr(true)})
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read API keys, got error: %s", err))
return
@@ -98,15 +124,50 @@ func (d *APIKeysDataSource) Read(ctx context.Context, req datasource.ReadRequest
return
}
+ objectType := types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "name": types.StringType,
+ "api_key": types.StringType,
+ },
+ }
+ var secretKeyObjects []attr.Value
+
for _, key := range *httpResp.JSON200 {
- switch key.Name {
- case "anon":
- data.AnonKey = NullableToString(key.ApiKey)
- case "service_role":
- data.ServiceRoleKey = NullableToString(key.ApiKey)
+ if key.Type.IsSpecified() && !key.Type.IsNull() {
+ keyType := key.Type.MustGet()
+
+ switch keyType {
+ case api.ApiKeyResponseTypeLegacy:
+ if key.Name == "anon" {
+ data.AnonKey = NullableToString(key.ApiKey)
+ }
+ if key.Name == "service_role" {
+ data.ServiceRoleKey = NullableToString(key.ApiKey)
+ }
+ case api.ApiKeyResponseTypePublishable:
+ data.PublishableKey = NullableToString(key.ApiKey)
+ case api.ApiKeyResponseTypeSecret:
+ obj, diags := types.ObjectValue(objectType.AttrTypes, map[string]attr.Value{
+ "name": types.StringValue(key.Name),
+ "api_key": NullableToString(key.ApiKey),
+ })
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ secretKeyObjects = append(secretKeyObjects, obj)
+ }
}
}
+ // Build list directly from object values
+ secretKeysList, diags := types.ListValue(objectType, secretKeyObjects)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ data.SecretKeys = secretKeysList
+
tflog.Trace(ctx, "read API keys")
// Save data into Terraform state
diff --git a/internal/provider/apikeys_data_source_test.go b/internal/provider/apikeys_data_source_test.go
index 64379ae..f691365 100644
--- a/internal/provider/apikeys_data_source_test.go
+++ b/internal/provider/apikeys_data_source_test.go
@@ -24,12 +24,29 @@ func TestAccProjectAPIKeysDataSource(t *testing.T) {
JSON([]api.ApiKeyResponse{
{
Name: "anon",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeLegacy),
ApiKey: nullable.NewNullableWithValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.anon"),
},
{
Name: "service_role",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeLegacy),
ApiKey: nullable.NewNullableWithValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.service_role"),
},
+ {
+ Name: "publishable",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypePublishable),
+ ApiKey: nullable.NewNullableWithValue("sb_publishable_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ },
+ {
+ Name: "secret",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeSecret),
+ ApiKey: nullable.NewNullableWithValue("sb_secret_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ },
+ {
+ Name: "other_secret",
+ Type: nullable.NewNullableWithValue(api.ApiKeyResponseTypeSecret),
+ ApiKey: nullable.NewNullableWithValue("sb_secret_eybcCI6kNiIsiR5UCJ9VGpzI1IciOJhJIXIn"),
+ },
})
resource.Test(t, resource.TestCase{
@@ -42,6 +59,12 @@ func TestAccProjectAPIKeysDataSource(t *testing.T) {
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.supabase_apikeys.production", "anon_key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.anon"),
resource.TestCheckResourceAttr("data.supabase_apikeys.production", "service_role_key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.service_role"),
+ resource.TestCheckResourceAttr("data.supabase_apikeys.production", "publishable_key", "sb_publishable_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ resource.TestCheckResourceAttr("data.supabase_apikeys.production", "secret_keys.#", "2"),
+ resource.TestCheckResourceAttr("data.supabase_apikeys.production", "secret_keys.0.name", "secret"),
+ resource.TestCheckResourceAttr("data.supabase_apikeys.production", "secret_keys.0.api_key", "sb_secret_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
+ resource.TestCheckResourceAttr("data.supabase_apikeys.production", "secret_keys.1.name", "other_secret"),
+ resource.TestCheckResourceAttr("data.supabase_apikeys.production", "secret_keys.1.api_key", "sb_secret_eybcCI6kNiIsiR5UCJ9VGpzI1IciOJhJIXIn"),
),
},
},
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 74a55e8..0b5e83a 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -89,6 +89,7 @@ func (p *SupabaseProvider) Resources(ctx context.Context) []func() resource.Reso
NewProjectResource,
NewSettingsResource,
NewBranchResource,
+ NewApiKeyResource,
}
}
diff --git a/internal/provider/utils.go b/internal/provider/utils.go
index 6522a81..b4f91cc 100644
--- a/internal/provider/utils.go
+++ b/internal/provider/utils.go
@@ -11,10 +11,9 @@ func Ptr[T any](v T) *T {
// NullableToString converts an oapi-codegen [nullable.Nullable] to an appropriate
// terraform string type.
-func NullableToString(n nullable.Nullable[string]) tftypes.String {
+func NullableToString[T ~string](n nullable.Nullable[T]) tftypes.String {
if n.IsSpecified() && !n.IsNull() {
- // MustGet is safe when the value is specified and not null
- return tftypes.StringValue(n.MustGet())
+ return tftypes.StringValue(string(n.MustGet()))
}
return tftypes.StringNull()