diff --git a/docs/data-sources/project_apikeys.md b/docs/data-sources/project_apikeys.md new file mode 100644 index 0000000..49756f0 --- /dev/null +++ b/docs/data-sources/project_apikeys.md @@ -0,0 +1,25 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "supabase_project_apikeys Data Source - terraform-provider-supabase" +subcategory: "" +description: |- + Project API Keys data source +--- + +# supabase_project_apikeys (Data Source) + +Project API Keys data source + + + + +## Schema + +### Required + +- `project_id` (String) Project identifier + +### Read-Only + +- `anon_key` (String, Sensitive) Anonymous API key for the project +- `service_role_key` (String, Sensitive) Service role API key for the project diff --git a/docs/schema.json b/docs/schema.json index a76d705..39525b0 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -288,6 +288,35 @@ "description": "Pooler data source", "description_kind": "markdown" } + }, + "supabase_project_apikeys": { + "version": 0, + "block": { + "attributes": { + "anon_key": { + "type": "string", + "description": "Anonymous API key for the project", + "description_kind": "markdown", + "computed": true, + "sensitive": true + }, + "project_id": { + "type": "string", + "description": "Project identifier", + "description_kind": "markdown", + "required": true + }, + "service_role_key": { + "type": "string", + "description": "Service role API key for the project", + "description_kind": "markdown", + "computed": true, + "sensitive": true + } + }, + "description": "Project API Keys data source", + "description_kind": "markdown" + } } } } diff --git a/docs/tutorial.md b/docs/tutorial.md index 7f01abc..b766f0c 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -41,11 +41,27 @@ resource "supabase_project" "production" { ignore_changes = [database_password] } } + +# Retrieve project API keys +data "supabase_project_apikeys" "production" { + project_id = supabase_project.production.id +} + +# Output the API keys (careful with sensitive data!) +output "anon_key" { + value = data.supabase_project_apikeys.production.anon_key + sensitive = true +} + +output "service_role_key" { + value = data.supabase_project_apikeys.production.service_role_key + sensitive = true +} ``` -Remember to substitute placeholder values with your own. For sensitive fields such as the password, consider storing and retrieving them from a secure credentials store. +Remember to substitute placeholder values with your own. For sensitive fields such as the password, consider storing and retrieving them from a secure credentials store. The API keys are marked as sensitive and will be hidden in logs, but make sure to handle them securely in your workflow. -Next, run `terraform -chdir=module apply` to create the new project resource. +Next, run `terraform -chdir=module apply` to create the new project resource and retrieve its API keys. ### Importing a project @@ -75,6 +91,11 @@ resource "supabase_project" "production" { ignore_changes = [database_password] } } + +# Retrieve project API keys +data "supabase_project_apikeys" "production" { + project_id = supabase_project.production.id +} ``` Run `terraform -chdir=module apply`. Enter the ID of your Supabase project at the prompt. If your local TF state is empty, your project will be imported from remote rather than recreated. diff --git a/internal/provider/project_apikeys_data_source.go b/internal/provider/project_apikeys_data_source.go new file mode 100644 index 0000000..062cb4a --- /dev/null +++ b/internal/provider/project_apikeys_data_source.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/supabase/cli/pkg/api" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &ProjectAPIKeysDataSource{} + +func NewProjectAPIKeysDataSource() datasource.DataSource { + return &ProjectAPIKeysDataSource{} +} + +// ProjectAPIKeysDataSource defines the data source implementation. +type ProjectAPIKeysDataSource struct { + client *api.ClientWithResponses +} + +// ProjectAPIKeysDataSourceModel describes the data source data model. +type ProjectAPIKeysDataSourceModel struct { + ProjectId types.String `tfsdk:"project_id"` + AnonKey types.String `tfsdk:"anon_key"` + ServiceRoleKey types.String `tfsdk:"service_role_key"` +} + +func (d *ProjectAPIKeysDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project_apikeys" +} + +func (d *ProjectAPIKeysDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Project API Keys data source", + + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + MarkdownDescription: "Project identifier", + Required: true, + }, + "anon_key": schema.StringAttribute{ + MarkdownDescription: "Anonymous API key for the project", + Computed: true, + Sensitive: true, + }, + "service_role_key": schema.StringAttribute{ + MarkdownDescription: "Service role API key for the project", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (d *ProjectAPIKeysDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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 Data Source Configure Type", + fmt.Sprintf("Expected *api.ClientWithResponses, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +func (d *ProjectAPIKeysDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data ProjectAPIKeysDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + httpResp, err := d.client.V1GetProjectApiKeysWithResponse(ctx, data.ProjectId.ValueString(), &api.V1GetProjectApiKeysParams{}) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read project API keys, got error: %s", err)) + return + } + + if httpResp.JSON200 == nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read project API keys, got status %d: %s", httpResp.StatusCode(), httpResp.Body)) + return + } + + for _, key := range *httpResp.JSON200 { + switch key.Name { + case "anon": + data.AnonKey = types.StringValue(key.ApiKey) + case "service_role": + data.ServiceRoleKey = types.StringValue(key.ApiKey) + } + } + + tflog.Trace(ctx, "read project API keys") + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/project_apikeys_data_source_test.go b/internal/provider/project_apikeys_data_source_test.go new file mode 100644 index 0000000..9797544 --- /dev/null +++ b/internal/provider/project_apikeys_data_source_test.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/supabase/cli/pkg/api" + "gopkg.in/h2non/gock.v1" +) + +func TestAccProjectAPIKeysDataSource(t *testing.T) { + // Setup mock api + defer gock.OffAll() + gock.New("https://api.supabase.com"). + Get("/v1/projects/mayuaycdtijbctgqbycg/api-keys"). + Times(3). + Reply(http.StatusOK). + JSON([]api.ApiKeyResponse{ + { + Name: "anon", + ApiKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.anon", + }, + { + Name: "service_role", + ApiKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.service_role", + }, + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Read testing + { + Config: testAccProjectAPIKeysDataSourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.supabase_project_apikeys.production", "anon_key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.anon"), + resource.TestCheckResourceAttr("data.supabase_project_apikeys.production", "service_role_key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.service_role"), + ), + }, + }, + }) +} + +const testAccProjectAPIKeysDataSourceConfig = ` +data "supabase_project_apikeys" "production" { + project_id = "mayuaycdtijbctgqbycg" +} +` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 49eafb8..4560081 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -96,6 +96,7 @@ func (p *SupabaseProvider) DataSources(ctx context.Context) []func() datasource. return []func() datasource.DataSource{ NewBranchDataSource, NewPoolerDataSource, + NewProjectAPIKeysDataSource, } }