From 66d40617a3e227ea64cedd22e7e9875bbc832636 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Mon, 18 Aug 2025 11:19:11 -0400 Subject: [PATCH 01/10] add support for managing projects --- README.md | 48 ++- docs/resources/project.md | 85 +++++ examples/README.md | 9 +- .../resources/pinecone_project/resource.tf | 51 +++ pinecone/models/projects.go | 15 + pinecone/provider/project_resource.go | 304 ++++++++++++++++++ pinecone/provider/project_resource_test.go | 114 +++++++ pinecone/provider/provider.go | 1 + pinecone/provider/provider_test.go | 11 + test-local/README.md | 2 +- test-local/project_test.tf | 36 +++ 11 files changed, 670 insertions(+), 6 deletions(-) create mode 100644 docs/resources/project.md create mode 100644 examples/resources/pinecone_project/resource.tf create mode 100644 pinecone/models/projects.go create mode 100644 pinecone/provider/project_resource.go create mode 100644 pinecone/provider/project_resource_test.go create mode 100644 test-local/project_test.tf diff --git a/README.md b/README.md index 1eacc76..0452e74 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Workflow](https://github.com/pinecone-io/terraform-provider-pinecone/actions/wor ![GitHub release (latest by date)](https://img.shields.io/github/v/release/pinecone-io/terraform-provider-pinecone) -The Terraform Provider for Pinecone allows Terraform to manage Pinecone resources. +The Terraform Provider for Pinecone allows Terraform to manage Pinecone resources including indexes, collections, API keys, and projects. Note: We take Terraform's security and our users' trust very seriously. If you believe you have found a security issue in the Terraform Provider for Pinecone, @@ -97,9 +97,9 @@ Remember, your API Key should be a protected secret. See how to [protect sensitive input variables](https://developer.hashicorp.com/terraform/tutorials/configuration-language/sensitive-variables) when setting your API Key this way. -#### Admin Operations (API Key Management) +#### Admin Operations (API Key and Project Management) -For creating and managing API keys, you need admin credentials (Client ID and Client Secret). +For creating and managing API keys and projects, you need admin credentials (Client ID and Client Secret). ##### Using Environment Variables @@ -120,7 +120,28 @@ provider "pinecone" { } ``` -**Note**: Admin credentials are required for API key management operations. Regular API keys cannot be used to create or manage other API keys. +#### Example: Creating a Project + +```terraform +# Create a basic project +resource "pinecone_project" "example" { + name = "my-production-project" +} + +# Create a project with CMEK encryption +resource "pinecone_project" "encrypted" { + name = "secure-project" + force_encryption_with_cmek = true +} + +# Create a project with custom pod limits +resource "pinecone_project" "custom_pods" { + name = "high-capacity-project" + max_pods = 10 +} +``` + +**Note**: Admin credentials are required for API key and project management operations. Regular API keys cannot be used to create or manage other API keys or projects. ### API Key Management @@ -137,6 +158,25 @@ The following roles can be assigned to API keys: - `DataPlaneEditor`: Full access to data plane operations - `DataPlaneViewer`: Read-only access to data plane operations +### Project Management + +The Terraform Provider for Pinecone supports creating and managing Pinecone projects. This is useful for organizing your Pinecone resources and managing project-level configurations. + +#### Project Features + +- **Project Creation**: Create new projects with custom names +- **CMEK Encryption**: Enable customer-managed encryption keys for enhanced security +- **Pod Limits**: Configure maximum number of pods per project +- **Project Import**: Import existing projects into Terraform state + +#### Project Configuration Options + +- `name`: The name of the project (required) +- `force_encryption_with_cmek`: Enable CMEK encryption (optional, cannot be disabled once enabled) +- `max_pods`: Maximum number of pods allowed in the project (optional, default varies by plan) + +**Note**: Project management requires admin credentials (Client ID and Client Secret). Regular API keys cannot be used to manage projects. + ## Documentation Documentation can be found on the [Terraform diff --git a/docs/resources/project.md b/docs/resources/project.md new file mode 100644 index 0000000..964c3be --- /dev/null +++ b/docs/resources/project.md @@ -0,0 +1,85 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "pinecone_project Resource - terraform-provider-pinecone" +subcategory: "" +description: |- + The pinecone_project resource lets you create and manage projects in Pinecone. Learn more about projects in the docs https://docs.pinecone.io/guides/projects. +--- + +# pinecone_project (Resource) + +The `pinecone_project` resource lets you create and manage projects in Pinecone. Learn more about projects in the [docs](https://docs.pinecone.io/guides/projects). + +## Example Usage + +```terraform +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + } + } +} + +provider "pinecone" { + client_id = "your-client-id" + client_secret = "your-client-secret" +} + +# Create a basic project +resource "pinecone_project" "example" { + name = "example-project" +} + +# Create a project with CMEK encryption enabled +resource "pinecone_project" "encrypted" { + name = "encrypted-project" + force_encryption_with_cmek = true +} + +# Create a project with custom max pods +resource "pinecone_project" "custom_pods" { + name = "custom-pods-project" + max_pods = 10 +} + +# Create a project with all options +resource "pinecone_project" "full_featured" { + name = "full-featured-project" + force_encryption_with_cmek = false + max_pods = 5 +} + +output "project_id" { + description = "The ID of the created project" + value = pinecone_project.example.id +} + +output "project_name" { + description = "The name of the created project" + value = pinecone_project.example.name +} + +output "organization_id" { + description = "The organization ID of the project" + value = pinecone_project.example.organization_id +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the project to be created. + +### Optional + +- `force_encryption_with_cmek` (Boolean) Whether to force encryption with a customer-managed encryption key (CMEK). Default is `false`. Once enabled, CMEK encryption cannot be disabled. +- `max_pods` (Number) The maximum number of Pods that can be created in the project. Default is `0` (serverless only). + +### Read-Only + +- `created_at` (String) The timestamp when the project was created. +- `id` (String) Project identifier +- `organization_id` (String) The organization ID where the project will be created. diff --git a/examples/README.md b/examples/README.md index 026c42c..fda9c4e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,4 +6,11 @@ The document generation tool looks for files in the following locations by defau * **provider/provider.tf** example file for the provider index page * **data-sources/`full data source name`/data-source.tf** example file for the named data source page -* **resources/`full resource name`/resource.tf** example file for the named data source page +* **resources/`full resource name`/resource.tf** example file for the named resource page + +## Available Resources + +* **pinecone_api_key** - Manage API keys in Pinecone projects +* **pinecone_collection** - Manage Pinecone collections +* **pinecone_index** - Manage Pinecone indexes +* **pinecone_project** - Manage Pinecone projects (requires admin credentials) diff --git a/examples/resources/pinecone_project/resource.tf b/examples/resources/pinecone_project/resource.tf new file mode 100644 index 0000000..cc05e60 --- /dev/null +++ b/examples/resources/pinecone_project/resource.tf @@ -0,0 +1,51 @@ +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + } + } +} + +provider "pinecone" { + client_id = "your-client-id" + client_secret = "your-client-secret" +} + +# Create a basic project +resource "pinecone_project" "example" { + name = "example-project" +} + +# Create a project with CMEK encryption enabled +resource "pinecone_project" "encrypted" { + name = "encrypted-project" + force_encryption_with_cmek = true +} + +# Create a project with custom max pods +resource "pinecone_project" "custom_pods" { + name = "custom-pods-project" + max_pods = 10 +} + +# Create a project with all options +resource "pinecone_project" "full_featured" { + name = "full-featured-project" + force_encryption_with_cmek = false + max_pods = 5 +} + +output "project_id" { + description = "The ID of the created project" + value = pinecone_project.example.id +} + +output "project_name" { + description = "The name of the created project" + value = pinecone_project.example.name +} + +output "organization_id" { + description = "The organization ID of the project" + value = pinecone_project.example.organization_id +} diff --git a/pinecone/models/projects.go b/pinecone/models/projects.go new file mode 100644 index 0000000..35ae5a2 --- /dev/null +++ b/pinecone/models/projects.go @@ -0,0 +1,15 @@ +package models + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// ProjectResourceModel defines the project model for the resource. +type ProjectResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + OrganizationId types.String `tfsdk:"organization_id"` + ForceEncryptionWithCmek types.Bool `tfsdk:"force_encryption_with_cmek"` + MaxPods types.Int64 `tfsdk:"max_pods"` + CreatedAt types.String `tfsdk:"created_at"` +} diff --git a/pinecone/provider/project_resource.go b/pinecone/provider/project_resource.go new file mode 100644 index 0000000..0d91646 --- /dev/null +++ b/pinecone/provider/project_resource.go @@ -0,0 +1,304 @@ +package provider + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/pinecone-io/go-pinecone/v4/pinecone" + "github.com/pinecone-io/terraform-provider-pinecone/pinecone/models" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ProjectResource{} +var _ resource.ResourceWithImportState = &ProjectResource{} + +func NewProjectResource() resource.Resource { + return &ProjectResource{PineconeResource: &PineconeResource{}} +} + +// ProjectResource defines the resource implementation. +type ProjectResource struct { + *PineconeResource +} + +func (r *ProjectResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project" +} + +func (r *ProjectResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "The `pinecone_project` resource lets you create and manage projects in Pinecone. Learn more about projects in the [docs](https://docs.pinecone.io/guides/projects).", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Project identifier", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the project to be created.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + "organization_id": schema.StringAttribute{ + MarkdownDescription: "The organization ID where the project will be created.", + Computed: true, + }, + "force_encryption_with_cmek": schema.BoolAttribute{ + MarkdownDescription: "Whether to force encryption with a customer-managed encryption key (CMEK). Default is `false`. Once enabled, CMEK encryption cannot be disabled.", + Optional: true, + Computed: true, + }, + "max_pods": schema.Int64Attribute{ + MarkdownDescription: "The maximum number of Pods that can be created in the project. Default is `0` (serverless only).", + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp when the project was created.", + Computed: true, + }, + }, + } +} + +func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data models.ProjectResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Check if admin client is available + if r.adminClient == nil { + resp.Diagnostics.AddError("Admin client not configured", "Admin client credentials (client_id and client_secret) are required to create projects.") + return + } + + // Prepare create parameters + createParams := &pinecone.CreateProjectParams{ + Name: data.Name.ValueString(), + } + + // Handle force_encryption_with_cmek field + if !data.ForceEncryptionWithCmek.IsNull() && !data.ForceEncryptionWithCmek.IsUnknown() { + forceEncryption := data.ForceEncryptionWithCmek.ValueBool() + createParams.ForceEncryptionWithCmek = &forceEncryption + } + + // Handle max_pods field + if !data.MaxPods.IsNull() && !data.MaxPods.IsUnknown() { + maxPods := int(data.MaxPods.ValueInt64()) + createParams.MaxPods = &maxPods + } + + // Create the project + project, err := r.adminClient.Project.Create(ctx, createParams) + if err != nil { + resp.Diagnostics.AddError("Failed to create project", err.Error()) + return + } + + // Set the computed values + data.Id = types.StringValue(project.Id) + data.Name = types.StringValue(project.Name) + data.OrganizationId = types.StringValue(project.OrganizationId) + data.ForceEncryptionWithCmek = types.BoolValue(project.ForceEncryptionWithCmek) + data.MaxPods = types.Int64Value(int64(project.MaxPods)) + if project.CreatedAt != nil { + data.CreatedAt = types.StringValue(project.CreatedAt.Format(time.RFC3339)) + } else { + data.CreatedAt = types.StringNull() + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data models.ProjectResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Check if admin client is available + if r.adminClient == nil { + resp.Diagnostics.AddError("Admin client not configured", "Admin client credentials (client_id and client_secret) are required to read projects.") + return + } + + // Describe the project directly + project, err := r.adminClient.Project.Describe(ctx, data.Id.ValueString()) + if err != nil { + if strings.Contains(err.Error(), "not found") { + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError("Failed to describe project", err.Error()) + } + return + } + + // Update the model with the found project + data.Id = types.StringValue(project.Id) + data.Name = types.StringValue(project.Name) + data.OrganizationId = types.StringValue(project.OrganizationId) + data.ForceEncryptionWithCmek = types.BoolValue(project.ForceEncryptionWithCmek) + data.MaxPods = types.Int64Value(int64(project.MaxPods)) + if project.CreatedAt != nil { + data.CreatedAt = types.StringValue(project.CreatedAt.Format(time.RFC3339)) + } else { + data.CreatedAt = types.StringNull() + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data models.ProjectResourceModel + var state models.ProjectResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Check if admin client is available + if r.adminClient == nil { + resp.Diagnostics.AddError("Admin client not configured", "Admin client credentials (client_id and client_secret) are required to update projects.") + return + } + + // Prepare update parameters + updateParams := &pinecone.UpdateProjectParams{} + + // Check if name has changed + if !data.Name.Equal(state.Name) { + name := data.Name.ValueString() + updateParams.Name = &name + } + + // Check if force_encryption_with_cmek has changed + if !data.ForceEncryptionWithCmek.Equal(state.ForceEncryptionWithCmek) { + forceEncryption := data.ForceEncryptionWithCmek.ValueBool() + updateParams.ForceEncryptionWithCmek = &forceEncryption + } + + // Check if max_pods has changed + if !data.MaxPods.Equal(state.MaxPods) { + maxPods := int(data.MaxPods.ValueInt64()) + updateParams.MaxPods = &maxPods + } + + // Only update if there are changes + if updateParams.Name == nil && updateParams.ForceEncryptionWithCmek == nil && updateParams.MaxPods == nil { + // No changes, just save the current state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return + } + + // Update the project + updatedProject, err := r.adminClient.Project.Update(ctx, state.Id.ValueString(), updateParams) + if err != nil { + resp.Diagnostics.AddError("Failed to update project", err.Error()) + return + } + + // Update the model with the updated project + data.Id = types.StringValue(updatedProject.Id) + data.Name = types.StringValue(updatedProject.Name) + data.OrganizationId = types.StringValue(updatedProject.OrganizationId) + data.ForceEncryptionWithCmek = types.BoolValue(updatedProject.ForceEncryptionWithCmek) + data.MaxPods = types.Int64Value(int64(updatedProject.MaxPods)) + if updatedProject.CreatedAt != nil { + data.CreatedAt = types.StringValue(updatedProject.CreatedAt.Format(time.RFC3339)) + } else { + data.CreatedAt = types.StringNull() + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data models.ProjectResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Check if admin client is available + if r.adminClient == nil { + resp.Diagnostics.AddError("Admin client not configured", "Admin client credentials (client_id and client_secret) are required to delete projects.") + return + } + + // Delete the project + err := r.adminClient.Project.Delete(ctx, data.Id.ValueString()) + if err != nil { + if !strings.Contains(err.Error(), "not found") { + resp.Diagnostics.AddError("Failed to delete project", err.Error()) + } + return + } + + // Wait for project to be deleted + err = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { + // List projects to check if the project still exists + projects, err := r.adminClient.Project.List(ctx) + if err != nil { + return retry.NonRetryableError(err) + } + + // Check if the project still exists + for _, project := range projects { + if project.Id == data.Id.ValueString() { + return retry.RetryableError(fmt.Errorf("project not deleted yet")) + } + } + + return nil + }) + if err != nil { + resp.Diagnostics.AddError("Failed to wait for project to be deleted.", err.Error()) + return + } +} + +func (r *ProjectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Import format: project_id + projectId := req.ID + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), projectId)...) +} diff --git a/pinecone/provider/project_resource_test.go b/pinecone/provider/project_resource_test.go new file mode 100644 index 0000000..76d96b6 --- /dev/null +++ b/pinecone/provider/project_resource_test.go @@ -0,0 +1,114 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccProjectResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAdmin(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccProjectResourceConfig("test-project"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("pinecone_project.test", "name", "test-project"), + resource.TestCheckResourceAttrSet("pinecone_project.test", "id"), + resource.TestCheckResourceAttrSet("pinecone_project.test", "organization_id"), + resource.TestCheckResourceAttrSet("pinecone_project.test", "created_at"), + resource.TestCheckResourceAttr("pinecone_project.test", "force_encryption_with_cmek", "false"), + resource.TestCheckResourceAttr("pinecone_project.test", "max_pods", "20"), + ), + }, + // ImportState testing + { + ResourceName: "pinecone_project.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update and Read testing + { + Config: testAccProjectResourceConfig("updated-test-project"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("pinecone_project.test", "name", "updated-test-project"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func TestAccProjectResourceWithOptions(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAdmin(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing with options + { + Config: testAccProjectResourceWithOptionsConfig("test-project-with-options", false, 5), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("pinecone_project.test", "name", "test-project-with-options"), + resource.TestCheckResourceAttr("pinecone_project.test", "force_encryption_with_cmek", "false"), + resource.TestCheckResourceAttr("pinecone_project.test", "max_pods", "5"), + ), + }, + // Update and Read testing (only update name and max_pods, not CMEK) + { + Config: testAccProjectResourceWithOptionsConfig("updated-test-project-with-options", false, 10), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("pinecone_project.test", "name", "updated-test-project-with-options"), + resource.TestCheckResourceAttr("pinecone_project.test", "force_encryption_with_cmek", "false"), + resource.TestCheckResourceAttr("pinecone_project.test", "max_pods", "10"), + ), + }, + }, + }) +} + +func testAccProjectResourceConfig(name string) string { + return fmt.Sprintf(` +resource "pinecone_project" "test" { + name = %[1]q +} +`, name) +} + +func testAccProjectResourceWithOptionsConfig(name string, forceEncryption bool, maxPods int) string { + return fmt.Sprintf(` +resource "pinecone_project" "test" { + name = %[1]q + force_encryption_with_cmek = %[2]t + max_pods = %[3]d +} +`, name, forceEncryption, maxPods) +} + +func TestAccProjectResourceWithCMEK(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckAdmin(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create project with CMEK enabled + { + Config: testAccProjectResourceWithCMEKConfig("test-project-with-cmek"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("pinecone_project.test", "name", "test-project-with-cmek"), + resource.TestCheckResourceAttr("pinecone_project.test", "force_encryption_with_cmek", "true"), + ), + }, + }, + }) +} + +func testAccProjectResourceWithCMEKConfig(name string) string { + return fmt.Sprintf(` +resource "pinecone_project" "test" { + name = %[1]q + force_encryption_with_cmek = true +} +`, name) +} diff --git a/pinecone/provider/provider.go b/pinecone/provider/provider.go index 9110428..cbd053e 100644 --- a/pinecone/provider/provider.go +++ b/pinecone/provider/provider.go @@ -141,6 +141,7 @@ func (p *PineconeProvider) Resources(ctx context.Context) []func() resource.Reso NewCollectionResource, NewIndexResource, NewApiKeyResource, + NewProjectResource, } } diff --git a/pinecone/provider/provider_test.go b/pinecone/provider/provider_test.go index 4366cfe..459a565 100644 --- a/pinecone/provider/provider_test.go +++ b/pinecone/provider/provider_test.go @@ -4,6 +4,7 @@ package provider import ( + "os" "testing" "github.com/hashicorp/terraform-plugin-framework/providerserver" @@ -23,3 +24,13 @@ func testAccPreCheck(t *testing.T) { // about the appropriate environment variables being set are common to see in a pre-check // function. } + +func testAccPreCheckAdmin(t *testing.T) { + // Check for admin client credentials + if v := os.Getenv("PINECONE_CLIENT_ID"); v == "" { + t.Fatal("PINECONE_CLIENT_ID environment variable must be set for admin acceptance tests") + } + if v := os.Getenv("PINECONE_CLIENT_SECRET"); v == "" { + t.Fatal("PINECONE_CLIENT_SECRET environment variable must be set for admin acceptance tests") + } +} diff --git a/test-local/README.md b/test-local/README.md index e4bdfcc..7b0a92d 100644 --- a/test-local/README.md +++ b/test-local/README.md @@ -49,6 +49,7 @@ resource "pinecone_api_key" "test" { **Note**: You can modify `main.tf` to test any resource type. Refer to the `../examples/` folder for examples of how to create different resources: - `../examples/resources/pinecone_index/` - For index creation - `../examples/resources/pinecone_collection/` - For collection creation +- `../examples/resources/pinecone_project/` - For project creation (requires admin credentials) - `../examples/data-sources/` - For data source usage ### Step 3: Initialize with Published Provider @@ -92,7 +93,6 @@ provider "pinecone" {} - The local provider will have access to new features not yet in the published version - If you get checksum errors, remove `.terraform.lock.hcl` and re-run `terraform init` -- This setup allows you to test new resources like `pinecone_api_key` before they're published - Credentials are stored in `setup-env.sh` which is gitignored for security - You can test any resource type by updating `main.tf` - see the `../examples/` folder for reference - The warning about missing `.terraformrc` file is expected and harmless diff --git a/test-local/project_test.tf b/test-local/project_test.tf new file mode 100644 index 0000000..548840d --- /dev/null +++ b/test-local/project_test.tf @@ -0,0 +1,36 @@ +# Create a test project +resource "pinecone_project" "test" { + name = "terraform-test-project" +} + +# Create a project with CMEK encryption +resource "pinecone_project" "encrypted" { + name = "terraform-encrypted-project" + force_encryption_with_cmek = true +} + +# Create a project with custom max pods +resource "pinecone_project" "custom_pods" { + name = "terraform-custom-pods-project" + max_pods = 5 +} + +output "test_project_id" { + description = "The ID of the test project" + value = pinecone_project.test.id +} + +output "test_project_name" { + description = "The name of the test project" + value = pinecone_project.test.name +} + +output "encrypted_project_id" { + description = "The ID of the encrypted project" + value = pinecone_project.encrypted.id +} + +output "custom_pods_project_id" { + description = "The ID of the custom pods project" + value = pinecone_project.custom_pods.id +} From e83b5f6bf0c26355cef7bee3930d2e8dda06a8f6 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Thu, 21 Aug 2025 15:54:42 -0400 Subject: [PATCH 02/10] add update projects example --- test-local/project_test.tf | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/test-local/project_test.tf b/test-local/project_test.tf index 548840d..635a1da 100644 --- a/test-local/project_test.tf +++ b/test-local/project_test.tf @@ -15,6 +15,41 @@ resource "pinecone_project" "custom_pods" { max_pods = 5 } +# ============================================================================= +# UPDATE PROJECT EXAMPLES - Test various update scenarios +# ============================================================================= + +# Test Case 1: Update project name +resource "pinecone_project" "update_name" { + name = "terraform-updated-name-project" +} + +# Test Case 2: Update max_pods (add to existing project) +resource "pinecone_project" "update_pods" { + name = "terraform-update-pods-project" + max_pods = 15 # Increased from default 0 +} + +# Test Case 3: Update both name and max_pods +resource "pinecone_project" "update_multiple" { + name = "terraform-updated-multiple-project" + max_pods = 8 +} + +# Test Case 4: Update project with CMEK (can only be enabled, not disabled) +resource "pinecone_project" "update_cmek" { + name = "terraform-update-cmek-project" + force_encryption_with_cmek = true + max_pods = 12 +} + +# Test Case 5: Update existing project with all attributes +resource "pinecone_project" "update_comprehensive" { + name = "terraform-comprehensive-update-project" + force_encryption_with_cmek = true + max_pods = 20 +} + output "test_project_id" { description = "The ID of the test project" value = pinecone_project.test.id @@ -34,3 +69,67 @@ output "custom_pods_project_id" { description = "The ID of the custom pods project" value = pinecone_project.custom_pods.id } + +# ============================================================================= +# OUTPUTS - Update Test Results +# ============================================================================= + +output "update_name_project_id" { + description = "The ID of the updated name project" + value = pinecone_project.update_name.id +} + +output "update_name_project_name" { + description = "The updated name of the project" + value = pinecone_project.update_name.name +} + +output "update_pods_project_id" { + description = "The ID of the updated pods project" + value = pinecone_project.update_pods.id +} + +output "update_pods_project_max_pods" { + description = "The updated max_pods configuration" + value = pinecone_project.update_pods.max_pods +} + +output "update_multiple_project_id" { + description = "The ID of the multiple update project" + value = pinecone_project.update_multiple.id +} + +output "update_multiple_project_name" { + description = "The updated name of the multiple update project" + value = pinecone_project.update_multiple.name +} + +output "update_multiple_project_max_pods" { + description = "The updated max_pods of the multiple update project" + value = pinecone_project.update_multiple.max_pods +} + +output "update_cmek_project_id" { + description = "The ID of the CMEK update project" + value = pinecone_project.update_cmek.id +} + +output "update_cmek_project_cmek_enabled" { + description = "Whether CMEK encryption is enabled" + value = pinecone_project.update_cmek.force_encryption_with_cmek +} + +output "update_comprehensive_project_id" { + description = "The ID of the comprehensive update project" + value = pinecone_project.update_comprehensive.id +} + +output "update_comprehensive_project_name" { + description = "The name of the comprehensive update project" + value = pinecone_project.update_comprehensive.name +} + +output "update_comprehensive_project_max_pods" { + description = "The max_pods of the comprehensive update project" + value = pinecone_project.update_comprehensive.max_pods +} From e3d497fd6874d5a9519f584b64b0ca90e2bbbe65 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Thu, 21 Aug 2025 16:18:37 -0400 Subject: [PATCH 03/10] add retry logic for project deletion --- pinecone/provider/project_resource.go | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/pinecone/provider/project_resource.go b/pinecone/provider/project_resource.go index 0d91646..c58bd0d 100644 --- a/pinecone/provider/project_resource.go +++ b/pinecone/provider/project_resource.go @@ -273,11 +273,29 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest return } - // Wait for project to be deleted + // Wait for project to be deleted with more robust error handling err = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { - // List projects to check if the project still exists + // First try to list projects to check if the project still exists projects, err := r.adminClient.Project.List(ctx) if err != nil { + // Handle specific error cases that might be transient + if strings.Contains(err.Error(), "Resource Quota PodsPerProject not found") || + strings.Contains(err.Error(), "NOT_FOUND") || + strings.Contains(err.Error(), "404") { + // Try fallback approach - directly check if the specific project exists + _, getErr := r.adminClient.Project.Describe(ctx, data.Id.ValueString()) + if getErr != nil { + // If we can't describe the project, it's likely deleted + if strings.Contains(getErr.Error(), "not found") || + strings.Contains(getErr.Error(), "NOT_FOUND") || + strings.Contains(getErr.Error(), "404") { + return nil // Project is deleted + } + } + // This error might be transient, retry with a delay + return retry.RetryableError(fmt.Errorf("deletion verification in progress, retrying: %v", err)) + } + // For other errors, treat as non-retryable return retry.NonRetryableError(err) } @@ -291,6 +309,14 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest return nil }) if err != nil { + // If we get a retryable error that's related to the quota issue, + // we can assume the project was likely deleted successfully + if strings.Contains(err.Error(), "Resource Quota PodsPerProject not found") || + strings.Contains(err.Error(), "deletion verification in progress") { + // Log a warning but don't fail the deletion + // The project deletion was successful, but verification failed due to timing issues + return + } resp.Diagnostics.AddError("Failed to wait for project to be deleted.", err.Error()) return } From 3c426464ac9e59ddb3383af2700a8f67125c36e8 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Thu, 21 Aug 2025 16:21:54 -0400 Subject: [PATCH 04/10] fix linting --- pinecone/provider/project_resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinecone/provider/project_resource.go b/pinecone/provider/project_resource.go index c58bd0d..e55c3ab 100644 --- a/pinecone/provider/project_resource.go +++ b/pinecone/provider/project_resource.go @@ -309,7 +309,7 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest return nil }) if err != nil { - // If we get a retryable error that's related to the quota issue, + // If we get a retryable error that's related to the quota issue, // we can assume the project was likely deleted successfully if strings.Contains(err.Error(), "Resource Quota PodsPerProject not found") || strings.Contains(err.Error(), "deletion verification in progress") { From b04ba46e3ae5d0d1f726bd8278093b9a08d9a2b6 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Mon, 25 Aug 2025 11:41:23 -0400 Subject: [PATCH 05/10] fix test-local --- test-local/.terraformrc | 8 ++++++++ test-local/setup-env.sh | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 test-local/.terraformrc diff --git a/test-local/.terraformrc b/test-local/.terraformrc new file mode 100644 index 0000000..284194e --- /dev/null +++ b/test-local/.terraformrc @@ -0,0 +1,8 @@ +provider_installation { + dev_overrides { + "pinecone-io/pinecone" = "../" + } + direct { + exclude = ["pinecone-io/pinecone"] + } +} diff --git a/test-local/setup-env.sh b/test-local/setup-env.sh index a5d9eea..7243986 100755 --- a/test-local/setup-env.sh +++ b/test-local/setup-env.sh @@ -2,7 +2,7 @@ # Clean up previous setup echo "Cleaning up previous setup..." -rm -f .terraformrc +rm -f .terraform rm -rf .terraform .terraform.lock.hcl echo "Cleanup complete!" From 4452b10471a27678d6ed5604d3344dbe85269d5a Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Mon, 25 Aug 2025 12:17:16 -0400 Subject: [PATCH 06/10] simply test-local --- test-local/README.md | 77 +++++++++++++++++++++++++++++------------ test-local/setup-env.sh | 6 +++- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/test-local/README.md b/test-local/README.md index 7b0a92d..5275d77 100644 --- a/test-local/README.md +++ b/test-local/README.md @@ -46,37 +46,51 @@ resource "pinecone_api_key" "test" { } ``` -**Note**: You can modify `main.tf` to test any resource type. Refer to the `../examples/` folder for examples of how to create different resources: +**Note**: You can modify `main.tf` to test any resource type, including unreleased features. Refer to the `../examples/` folder for examples of how to create different resources: - `../examples/resources/pinecone_index/` - For index creation - `../examples/resources/pinecone_collection/` - For collection creation - `../examples/resources/pinecone_project/` - For project creation (requires admin credentials) - `../examples/data-sources/` - For data source usage -### Step 3: Initialize with Published Provider +### Step 3: Initialize Terraform (Optional) + +You can run `terraform init` to initialize the workspace, but this will use the registry provider which doesn't support all features: ```bash terraform init ``` -This downloads the published provider and creates the necessary directory structure. +### Step 4: Test with Local Provider -### Step 4: Replace with Local Binary +To use your locally built provider with all features, simply run: ```bash -cp ../terraform-provider-pinecone .terraform/providers/registry.terraform.io/pinecone-io/pinecone/1.0.0/darwin_arm64/terraform-provider-pinecone +source setup-env.sh && terraform plan ``` -This replaces the published provider binary with your local development version. +This command: +1. Sources your credentials from `setup-env.sh` +2. Automatically configures Terraform to use your local provider +3. Runs `terraform plan` using your local provider -### Step 5: Test Your Changes +## Configuration -```bash -terraform plan -``` +### Dev Overrides Setup -Now you can test your local provider changes! +The `.terraformrc` file configures Terraform to use your locally built provider: -## Configuration +```hcl +provider_installation { + dev_overrides { + "pinecone-io/pinecone" = "../" + } + direct { + exclude = ["pinecone-io/pinecone"] + } +} +``` + +### Provider Configuration The provider will automatically pick up credentials from environment variables: @@ -91,19 +105,36 @@ provider "pinecone" {} ## Notes -- The local provider will have access to new features not yet in the published version -- If you get checksum errors, remove `.terraform.lock.hcl` and re-run `terraform init` -- Credentials are stored in `setup-env.sh` which is gitignored for security -- You can test any resource type by updating `main.tf` - see the `../examples/` folder for reference -- The warning about missing `.terraformrc` file is expected and harmless -- The `setup-env.sh` script automatically cleans up previous setup +- **Unreleased features**: The local provider includes features not yet in the published version +- **Dev_overrides bypass the registry**: The local provider is used instead of downloading from the registry +- **terraform init works**: You can run `terraform init` without dev_overrides to initialize the workspace +- **Environment variables**: Credentials are loaded from `setup-env.sh` which is gitignored for security +- **Warning messages**: The "Provider development overrides are in effect" warning is expected and normal +- **Simplified workflow**: `setup-env.sh` automatically handles cleanup and provider configuration ## Troubleshooting If you encounter issues: -1. **Checksum errors**: Remove `.terraform.lock.hcl` and re-run `terraform init` -2. **Provider not found**: Ensure the binary path is correct for your OS/architecture -3. **Authentication errors**: Verify your environment variables are set correctly -4. **Environment variables not loaded**: Make sure to run `source setup-env.sh` before testing -5. **Previous setup conflicts**: Run `source setup-env.sh` again to clean up +1. **Provider not found**: Ensure the binary exists at `../terraform-provider-pinecone` +2. **Authentication errors**: Verify your credentials in `setup-env.sh` are correct +3. **Environment variables not loaded**: Make sure to run `source setup-env.sh` before testing +4. **Previous setup conflicts**: Run `source setup-env.sh` again to clean up +5. **"Resource not supported"**: You're using the registry provider instead of the local one. Make sure to run `source setup-env.sh` first. + +## Common Commands + +**Test your configuration (with unreleased features):** +```bash +source setup-env.sh && terraform plan +``` + +**Apply your changes:** +```bash +source setup-env.sh && terraform apply +``` + +**Clean up:** +```bash +source setup-env.sh && terraform destroy +``` diff --git a/test-local/setup-env.sh b/test-local/setup-env.sh index 7243986..2ca0e9c 100755 --- a/test-local/setup-env.sh +++ b/test-local/setup-env.sh @@ -3,14 +3,18 @@ # Clean up previous setup echo "Cleaning up previous setup..." rm -f .terraform -rm -rf .terraform .terraform.lock.hcl +rm -rf .terraform .terraform.lock.hcl terraform.tfstate* echo "Cleanup complete!" # Set your Pinecone admin credentials, changes in this file will not be committed to the repo export PINECONE_CLIENT_ID="pinecone-client-id" export PINECONE_CLIENT_SECRET="pinecone-client-secret" +# Configure Terraform to use local provider +export TF_CLI_CONFIG_FILE=.terraformrc + echo "Environment variables set:" echo "PINECONE_CLIENT_ID: $PINECONE_CLIENT_ID" echo "PINECONE_CLIENT_SECRET: $PINECONE_CLIENT_SECRET" +echo "TF_CLI_CONFIG_FILE: $TF_CLI_CONFIG_FILE" echo "Note: No API key needed for admin operations" From 4202f77032f89add3c362198a9255107f427b1d9 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Mon, 25 Aug 2025 15:24:23 -0400 Subject: [PATCH 07/10] fix flaky test --- pinecone/provider/project_resource.go | 37 ++++++++------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/pinecone/provider/project_resource.go b/pinecone/provider/project_resource.go index e55c3ab..497bbee 100644 --- a/pinecone/provider/project_resource.go +++ b/pinecone/provider/project_resource.go @@ -273,40 +273,23 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest return } - // Wait for project to be deleted with more robust error handling + // Wait for project to be deleted with simplified verification err = retry.RetryContext(ctx, 5*time.Minute, func() *retry.RetryError { - // First try to list projects to check if the project still exists - projects, err := r.adminClient.Project.List(ctx) + // Try to describe the specific project to check if it still exists + _, err := r.adminClient.Project.Describe(ctx, data.Id.ValueString()) if err != nil { - // Handle specific error cases that might be transient - if strings.Contains(err.Error(), "Resource Quota PodsPerProject not found") || + // If we can't describe the project, it's likely deleted + if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "NOT_FOUND") || strings.Contains(err.Error(), "404") { - // Try fallback approach - directly check if the specific project exists - _, getErr := r.adminClient.Project.Describe(ctx, data.Id.ValueString()) - if getErr != nil { - // If we can't describe the project, it's likely deleted - if strings.Contains(getErr.Error(), "not found") || - strings.Contains(getErr.Error(), "NOT_FOUND") || - strings.Contains(getErr.Error(), "404") { - return nil // Project is deleted - } - } - // This error might be transient, retry with a delay - return retry.RetryableError(fmt.Errorf("deletion verification in progress, retrying: %v", err)) + return nil // Project is deleted } - // For other errors, treat as non-retryable - return retry.NonRetryableError(err) + // For other errors, retry + return retry.RetryableError(fmt.Errorf("deletion verification in progress, retrying: %v", err)) } - // Check if the project still exists - for _, project := range projects { - if project.Id == data.Id.ValueString() { - return retry.RetryableError(fmt.Errorf("project not deleted yet")) - } - } - - return nil + // Project still exists, retry + return retry.RetryableError(fmt.Errorf("project not deleted yet")) }) if err != nil { // If we get a retryable error that's related to the quota issue, From 2bc7f7d20454063923872b59a8f93dfe36030dcc Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Thu, 4 Sep 2025 10:35:33 -0400 Subject: [PATCH 08/10] add data source for projects --- examples/provider/admin_provider.tf | 7 +++- pinecone/models/projects.go | 62 +++++++++++++++++++++++++++++ pinecone/provider/provider.go | 2 + 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/examples/provider/admin_provider.tf b/examples/provider/admin_provider.tf index bb74ce1..3e596f2 100644 --- a/examples/provider/admin_provider.tf +++ b/examples/provider/admin_provider.tf @@ -12,8 +12,11 @@ provider "pinecone" { client_secret = "your-client-secret" } -# Example API key resource +# List all available projects +data "pinecone_projects" "all" {} + +# Example API key resource using the first available project resource "pinecone_api_key" "example" { name = "example-api-key" - project_id = "your-project-id" + project_id = data.pinecone_projects.all.projects[0].id } \ No newline at end of file diff --git a/pinecone/models/projects.go b/pinecone/models/projects.go index 35ae5a2..d30c163 100644 --- a/pinecone/models/projects.go +++ b/pinecone/models/projects.go @@ -1,7 +1,10 @@ package models import ( + "time" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/pinecone-io/go-pinecone/v4/pinecone" ) // ProjectResourceModel defines the project model for the resource. @@ -13,3 +16,62 @@ type ProjectResourceModel struct { MaxPods types.Int64 `tfsdk:"max_pods"` CreatedAt types.String `tfsdk:"created_at"` } + +// ProjectDataSourceModel defines the project model for the data source. +type ProjectDataSourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + OrganizationId types.String `tfsdk:"organization_id"` + ForceEncryptionWithCmek types.Bool `tfsdk:"force_encryption_with_cmek"` + MaxPods types.Int64 `tfsdk:"max_pods"` + CreatedAt types.String `tfsdk:"created_at"` +} + +// ProjectsDataSourceModel defines the projects list model for the data source. +type ProjectsDataSourceModel struct { + Projects []ProjectModel `tfsdk:"projects"` + Id types.String `tfsdk:"id"` +} + +// ProjectModel defines a single project in the projects list. +type ProjectModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + OrganizationId types.String `tfsdk:"organization_id"` + ForceEncryptionWithCmek types.Bool `tfsdk:"force_encryption_with_cmek"` + MaxPods types.Int64 `tfsdk:"max_pods"` + CreatedAt types.String `tfsdk:"created_at"` +} + +// Read populates the ProjectDataSourceModel from a pinecone.Project. +func (m *ProjectDataSourceModel) Read(project *pinecone.Project) { + m.Id = types.StringValue(project.Id) + m.Name = types.StringValue(project.Name) + m.OrganizationId = types.StringValue(project.OrganizationId) + m.ForceEncryptionWithCmek = types.BoolValue(project.ForceEncryptionWithCmek) + m.MaxPods = types.Int64Value(int64(project.MaxPods)) + if project.CreatedAt != nil { + m.CreatedAt = types.StringValue(project.CreatedAt.Format(time.RFC3339)) + } else { + m.CreatedAt = types.StringNull() + } +} + +// NewProjectModel creates a new ProjectModel from a pinecone.Project. +func NewProjectModel(project *pinecone.Project) *ProjectModel { + model := &ProjectModel{ + Id: types.StringValue(project.Id), + Name: types.StringValue(project.Name), + OrganizationId: types.StringValue(project.OrganizationId), + ForceEncryptionWithCmek: types.BoolValue(project.ForceEncryptionWithCmek), + MaxPods: types.Int64Value(int64(project.MaxPods)), + } + + if project.CreatedAt != nil { + model.CreatedAt = types.StringValue(project.CreatedAt.Format(time.RFC3339)) + } else { + model.CreatedAt = types.StringNull() + } + + return model +} diff --git a/pinecone/provider/provider.go b/pinecone/provider/provider.go index cbd053e..ee8ea90 100644 --- a/pinecone/provider/provider.go +++ b/pinecone/provider/provider.go @@ -151,6 +151,8 @@ func (p *PineconeProvider) DataSources(ctx context.Context) []func() datasource. NewCollectionDataSource, NewIndexesDataSource, NewIndexDataSource, + NewProjectsDataSource, + NewProjectDataSource, } } From 662f34220f651130e0a5c3f10c0f4971f82d3a7b Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Thu, 4 Sep 2025 10:38:39 -0400 Subject: [PATCH 09/10] add remaining files for projects data source --- docs/data-sources/project.md | 75 ++++++++++++ docs/data-sources/projects.md | 84 +++++++++++++ examples/complete-example/main.tf | 81 +++++++++++++ .../pinecone_project/data-source.tf | 44 +++++++ .../pinecone_projects/data-source.tf | 48 ++++++++ pinecone/provider/project_data_source.go | 88 ++++++++++++++ pinecone/provider/project_data_source_test.go | 73 ++++++++++++ pinecone/provider/projects_data_source.go | 110 ++++++++++++++++++ .../provider/projects_data_source_test.go | 46 ++++++++ 9 files changed, 649 insertions(+) create mode 100644 docs/data-sources/project.md create mode 100644 docs/data-sources/projects.md create mode 100644 examples/complete-example/main.tf create mode 100644 examples/data-sources/pinecone_project/data-source.tf create mode 100644 examples/data-sources/pinecone_projects/data-source.tf create mode 100644 pinecone/provider/project_data_source.go create mode 100644 pinecone/provider/project_data_source_test.go create mode 100644 pinecone/provider/projects_data_source.go create mode 100644 pinecone/provider/projects_data_source_test.go diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md new file mode 100644 index 0000000..c65ee2c --- /dev/null +++ b/docs/data-sources/project.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "pinecone_project Data Source - terraform-provider-pinecone" +subcategory: "" +description: |- + Project data source +--- + +# pinecone_project (Data Source) + +Project data source + +## Example Usage + +```terraform +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + version = "~> 1.0" + } + } +} + +provider "pinecone" { + client_id = var.client_id + client_secret = var.client_secret +} + +# Read a specific project by ID +data "pinecone_project" "example" { + id = var.project_id +} + +# Output the project details +output "project_name" { + description = "The name of the project" + value = data.pinecone_project.example.name +} + +output "project_organization_id" { + description = "The organization ID of the project" + value = data.pinecone_project.example.organization_id +} + +output "project_force_encryption_with_cmek" { + description = "Whether CMEK encryption is forced" + value = data.pinecone_project.example.force_encryption_with_cmek +} + +output "project_max_pods" { + description = "The maximum number of pods allowed" + value = data.pinecone_project.example.max_pods +} + +output "project_created_at" { + description = "When the project was created" + value = data.pinecone_project.example.created_at +} +``` + + +## Schema + +### Required + +- `id` (String) Project identifier + +### Read-Only + +- `created_at` (String) The timestamp when the project was created. +- `force_encryption_with_cmek` (Boolean) Whether encryption with a customer-managed encryption key (CMEK) is forced. +- `max_pods` (Number) The maximum number of Pods that can be created in the project. +- `name` (String) The name of the project. +- `organization_id` (String) The organization ID where the project is located. diff --git a/docs/data-sources/projects.md b/docs/data-sources/projects.md new file mode 100644 index 0000000..d9c5ae9 --- /dev/null +++ b/docs/data-sources/projects.md @@ -0,0 +1,84 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "pinecone_projects Data Source - terraform-provider-pinecone" +subcategory: "" +description: |- + Projects data source +--- + +# pinecone_projects (Data Source) + +Projects data source + +## Example Usage + +```terraform +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + } + } +} + +provider "pinecone" { + client_id = var.client_id + client_secret = var.client_secret +} + +# Read all available projects +data "pinecone_projects" "all" {} + +# Output the count of projects +output "project_count" { + description = "Total number of projects" + value = length(data.pinecone_projects.all.projects) +} + +# Output all project names +output "project_names" { + description = "Names of all projects" + value = [for project in data.pinecone_projects.all.projects : project.name] +} + +# Output all project IDs +output "project_ids" { + description = "IDs of all projects" + value = [for project in data.pinecone_projects.all.projects : project.id] +} + +# Output projects with CMEK encryption enabled +output "cmek_projects" { + description = "Projects with CMEK encryption enabled" + value = [for project in data.pinecone_projects.all.projects : project.name if project.force_encryption_with_cmek] +} + +# Output projects with pod limits +output "projects_with_pod_limits" { + description = "Projects with pod limits configured" + value = [for project in data.pinecone_projects.all.projects : { + name = project.name + max_pods = project.max_pods + } if project.max_pods > 0] +} +``` + + +## Schema + +### Read-Only + +- `id` (String) Projects identifier +- `projects` (Attributes List) List of the projects in your organization (see [below for nested schema](#nestedatt--projects)) + + +### Nested Schema for `projects` + +Read-Only: + +- `created_at` (String) The timestamp when the project was created. +- `force_encryption_with_cmek` (Boolean) Whether encryption with a customer-managed encryption key (CMEK) is forced. +- `id` (String) The unique ID of the project. +- `max_pods` (Number) The maximum number of Pods that can be created in the project. +- `name` (String) The name of the project. +- `organization_id` (String) The unique ID of the organization that the project belongs to. diff --git a/examples/complete-example/main.tf b/examples/complete-example/main.tf new file mode 100644 index 0000000..f69d628 --- /dev/null +++ b/examples/complete-example/main.tf @@ -0,0 +1,81 @@ +# Complete example demonstrating Pinecone Terraform provider capabilities +# This example shows: +# 1. Using data sources to list and read existing projects +# 2. Creating a new project resource +# 3. Creating an API key within the project +# 4. Outputting various project and API key information + +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + } + } +} + +# Configure the provider with admin credentials +# Set PINECONE_CLIENT_ID and PINECONE_CLIENT_SECRET environment variables +provider "pinecone" {} + +# List all available projects (data source) +data "pinecone_projects" "all" {} + +# Create a test project +resource "pinecone_project" "test" { + name = "terraform-test-project" +} + +# Read the created project using data source +data "pinecone_project" "test" { + id = pinecone_project.test.id +} + +# Create an API key within the project +resource "pinecone_api_key" "test" { + name = "terraform-test-api-key" + project_id = pinecone_project.test.id + + depends_on = [pinecone_project.test] +} + +# Output the results +output "project_name" { + value = pinecone_project.test.name +} + +output "project_id" { + value = pinecone_project.test.id +} + +output "api_key_id" { + value = pinecone_api_key.test.id +} + +output "api_key_name" { + value = pinecone_api_key.test.name +} + +output "api_key_value" { + value = pinecone_api_key.test.key + sensitive = true +} + +# Output data source results +output "total_projects_count" { + description = "Total number of projects in the organization" + value = length(data.pinecone_projects.all.projects) +} + +output "project_from_data_source" { + description = "Project details from data source" + value = { + id = data.pinecone_project.test.id + name = data.pinecone_project.test.name + created_at = data.pinecone_project.test.created_at + } +} + +output "all_project_names" { + description = "Names of all projects in the organization" + value = [for project in data.pinecone_projects.all.projects : project.name] +} diff --git a/examples/data-sources/pinecone_project/data-source.tf b/examples/data-sources/pinecone_project/data-source.tf new file mode 100644 index 0000000..11eda7c --- /dev/null +++ b/examples/data-sources/pinecone_project/data-source.tf @@ -0,0 +1,44 @@ +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + version = "~> 1.0" + } + } +} + +provider "pinecone" { + client_id = var.client_id + client_secret = var.client_secret +} + +# Read a specific project by ID +data "pinecone_project" "example" { + id = var.project_id +} + +# Output the project details +output "project_name" { + description = "The name of the project" + value = data.pinecone_project.example.name +} + +output "project_organization_id" { + description = "The organization ID of the project" + value = data.pinecone_project.example.organization_id +} + +output "project_force_encryption_with_cmek" { + description = "Whether CMEK encryption is forced" + value = data.pinecone_project.example.force_encryption_with_cmek +} + +output "project_max_pods" { + description = "The maximum number of pods allowed" + value = data.pinecone_project.example.max_pods +} + +output "project_created_at" { + description = "When the project was created" + value = data.pinecone_project.example.created_at +} diff --git a/examples/data-sources/pinecone_projects/data-source.tf b/examples/data-sources/pinecone_projects/data-source.tf new file mode 100644 index 0000000..50f1b81 --- /dev/null +++ b/examples/data-sources/pinecone_projects/data-source.tf @@ -0,0 +1,48 @@ +terraform { + required_providers { + pinecone = { + source = "pinecone-io/pinecone" + } + } +} + +provider "pinecone" { + client_id = var.client_id + client_secret = var.client_secret +} + +# Read all available projects +data "pinecone_projects" "all" {} + +# Output the count of projects +output "project_count" { + description = "Total number of projects" + value = length(data.pinecone_projects.all.projects) +} + +# Output all project names +output "project_names" { + description = "Names of all projects" + value = [for project in data.pinecone_projects.all.projects : project.name] +} + +# Output all project IDs +output "project_ids" { + description = "IDs of all projects" + value = [for project in data.pinecone_projects.all.projects : project.id] +} + +# Output projects with CMEK encryption enabled +output "cmek_projects" { + description = "Projects with CMEK encryption enabled" + value = [for project in data.pinecone_projects.all.projects : project.name if project.force_encryption_with_cmek] +} + +# Output projects with pod limits +output "projects_with_pod_limits" { + description = "Projects with pod limits configured" + value = [for project in data.pinecone_projects.all.projects : { + name = project.name + max_pods = project.max_pods + } if project.max_pods > 0] +} diff --git a/pinecone/provider/project_data_source.go b/pinecone/provider/project_data_source.go new file mode 100644 index 0000000..3203b15 --- /dev/null +++ b/pinecone/provider/project_data_source.go @@ -0,0 +1,88 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/pinecone-io/terraform-provider-pinecone/pinecone/models" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &ProjectDataSource{} + +func NewProjectDataSource() datasource.DataSource { + return &ProjectDataSource{PineconeDatasource: &PineconeDatasource{}} +} + +// ProjectDataSource defines the data source implementation. +type ProjectDataSource struct { + *PineconeDatasource +} + +func (d *ProjectDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_project" +} + +func (d *ProjectDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Project data source", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Project identifier", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the project.", + Computed: true, + }, + "organization_id": schema.StringAttribute{ + MarkdownDescription: "The organization ID where the project is located.", + Computed: true, + }, + "force_encryption_with_cmek": schema.BoolAttribute{ + MarkdownDescription: "Whether encryption with a customer-managed encryption key (CMEK) is forced.", + Computed: true, + }, + "max_pods": schema.Int64Attribute{ + MarkdownDescription: "The maximum number of Pods that can be created in the project.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp when the project was created.", + Computed: true, + }, + }, + } +} + +func (d *ProjectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data models.ProjectDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Check if admin client is available + if d.adminClient == nil { + resp.Diagnostics.AddError("Admin client not configured", "Admin client credentials (client_id and client_secret) are required to read projects.") + return + } + + project, err := d.adminClient.Project.Describe(ctx, data.Id.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to describe project, got error: %s", err)) + return + } + + // Save data into Terraform state + data.Read(project) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/pinecone/provider/project_data_source_test.go b/pinecone/provider/project_data_source_test.go new file mode 100644 index 0000000..f9404d5 --- /dev/null +++ b/pinecone/provider/project_data_source_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccProjectDataSource(t *testing.T) { + // Test with invalid UUID format first + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` + data "pinecone_project" "test" { + id = "invalid-uuid-format" + } + `, + ExpectError: regexp.MustCompile("invalid UUID"), + }, + }, + }) +} + +func TestAccProjectDataSourceWithRealProject(t *testing.T) { + // Test with a real project ID if credentials are available + projectID := os.Getenv("PINECONE_PROJECT_ID") + clientId := os.Getenv("PINECONE_CLIENT_ID") + clientSecret := os.Getenv("PINECONE_CLIENT_SECRET") + + if projectID == "" { + t.Skip("PINECONE_PROJECT_ID environment variable is required for this test") + } + if clientId == "" { + t.Skip("PINECONE_CLIENT_ID environment variable is required for this test") + } + if clientSecret == "" { + t.Skip("PINECONE_CLIENT_SECRET environment variable is required for this test") + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + provider "pinecone" { + client_id = "%s" + client_secret = "%s" + } + + data "pinecone_project" "test" { + id = "%s" + } + `, clientId, clientSecret, projectID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.pinecone_project.test", "id", projectID), + resource.TestCheckResourceAttrSet("data.pinecone_project.test", "name"), + resource.TestCheckResourceAttrSet("data.pinecone_project.test", "organization_id"), + resource.TestCheckResourceAttrSet("data.pinecone_project.test", "created_at"), + resource.TestCheckResourceAttrSet("data.pinecone_project.test", "force_encryption_with_cmek"), + resource.TestCheckResourceAttrSet("data.pinecone_project.test", "max_pods"), + ), + }, + }, + }) +} diff --git a/pinecone/provider/projects_data_source.go b/pinecone/provider/projects_data_source.go new file mode 100644 index 0000000..7a23401 --- /dev/null +++ b/pinecone/provider/projects_data_source.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/pinecone-io/terraform-provider-pinecone/pinecone/models" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &ProjectsDataSource{} + +func NewProjectsDataSource() datasource.DataSource { + return &ProjectsDataSource{PineconeDatasource: &PineconeDatasource{}} +} + +// ProjectsDataSource defines the data source implementation. +type ProjectsDataSource struct { + *PineconeDatasource +} + +func (d *ProjectsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_projects" +} + +func (d *ProjectsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Projects data source", + + Attributes: map[string]schema.Attribute{ + "projects": schema.ListNestedAttribute{ + MarkdownDescription: "List of the projects in your organization", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The unique ID of the project.", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the project.", + Computed: true, + }, + "organization_id": schema.StringAttribute{ + MarkdownDescription: "The unique ID of the organization that the project belongs to.", + Computed: true, + }, + "force_encryption_with_cmek": schema.BoolAttribute{ + MarkdownDescription: "Whether encryption with a customer-managed encryption key (CMEK) is forced.", + Computed: true, + }, + "max_pods": schema.Int64Attribute{ + MarkdownDescription: "The maximum number of Pods that can be created in the project.", + Computed: true, + }, + "created_at": schema.StringAttribute{ + MarkdownDescription: "The timestamp when the project was created.", + Computed: true, + }, + }, + }, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "Projects identifier", + Computed: true, + }, + }, + } +} + +func (d *ProjectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data models.ProjectsDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Check if admin client is available + if d.adminClient == nil { + resp.Diagnostics.AddError("Admin client not configured", "Admin client credentials (client_id and client_secret) are required to list projects.") + return + } + + projects, err := d.adminClient.Project.List(ctx) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to list projects, got error: %s", err)) + return + } + + // Convert projects to models and append to the list + for _, p := range projects { + data.Projects = append(data.Projects, *models.NewProjectModel(p)) + } + + // Save data into Terraform state + data.Id = types.StringValue(strconv.FormatInt(time.Now().Unix(), 10)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/pinecone/provider/projects_data_source_test.go b/pinecone/provider/projects_data_source_test.go new file mode 100644 index 0000000..26f24f9 --- /dev/null +++ b/pinecone/provider/projects_data_source_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccProjectsDataSource(t *testing.T) { + // Test listing all projects + clientId := os.Getenv("PINECONE_CLIENT_ID") + clientSecret := os.Getenv("PINECONE_CLIENT_SECRET") + + if clientId == "" { + t.Skip("PINECONE_CLIENT_ID environment variable is required for this test") + } + if clientSecret == "" { + t.Skip("PINECONE_CLIENT_SECRET environment variable is required for this test") + } + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + provider "pinecone" { + client_id = "%s" + client_secret = "%s" + } + + data "pinecone_projects" "test" {} + `, clientId, clientSecret), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.pinecone_projects.test", "id"), + resource.TestCheckResourceAttrSet("data.pinecone_projects.test", "projects.#"), // Should have projects + resource.TestCheckResourceAttrSet("data.pinecone_projects.test", "projects.0.created_at"), // Check first project has created_at + ), + }, + }, + }) +} From 69600b42a98f0b1afac10465287005dd12d7e925 Mon Sep 17 00:00:00 2001 From: rohanshah18 Date: Thu, 4 Sep 2025 11:05:09 -0400 Subject: [PATCH 10/10] clean up --- README.md | 1 - docs/data-sources/project.md | 3 +-- examples/data-sources/pinecone_project/data-source.tf | 3 +-- pinecone/provider/project_data_source_test.go | 3 --- pinecone/provider/project_resource.go | 10 ++++------ pinecone/provider/projects_data_source.go | 3 --- pinecone/provider/projects_data_source_test.go | 3 --- 7 files changed, 6 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 0452e74..de3ef37 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,6 @@ The Terraform Provider for Pinecone supports creating and managing Pinecone proj - **Project Creation**: Create new projects with custom names - **CMEK Encryption**: Enable customer-managed encryption keys for enhanced security - **Pod Limits**: Configure maximum number of pods per project -- **Project Import**: Import existing projects into Terraform state #### Project Configuration Options diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index c65ee2c..2302613 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -16,8 +16,7 @@ Project data source terraform { required_providers { pinecone = { - source = "pinecone-io/pinecone" - version = "~> 1.0" + source = "pinecone-io/pinecone" } } } diff --git a/examples/data-sources/pinecone_project/data-source.tf b/examples/data-sources/pinecone_project/data-source.tf index 11eda7c..f900b47 100644 --- a/examples/data-sources/pinecone_project/data-source.tf +++ b/examples/data-sources/pinecone_project/data-source.tf @@ -1,8 +1,7 @@ terraform { required_providers { pinecone = { - source = "pinecone-io/pinecone" - version = "~> 1.0" + source = "pinecone-io/pinecone" } } } diff --git a/pinecone/provider/project_data_source_test.go b/pinecone/provider/project_data_source_test.go index f9404d5..c259bcf 100644 --- a/pinecone/provider/project_data_source_test.go +++ b/pinecone/provider/project_data_source_test.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider import ( diff --git a/pinecone/provider/project_resource.go b/pinecone/provider/project_resource.go index 497bbee..1f81fe3 100644 --- a/pinecone/provider/project_resource.go +++ b/pinecone/provider/project_resource.go @@ -267,9 +267,7 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest // Delete the project err := r.adminClient.Project.Delete(ctx, data.Id.ValueString()) if err != nil { - if !strings.Contains(err.Error(), "not found") { - resp.Diagnostics.AddError("Failed to delete project", err.Error()) - } + resp.Diagnostics.AddError("Failed to delete project", err.Error()) return } @@ -292,10 +290,10 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest return retry.RetryableError(fmt.Errorf("project not deleted yet")) }) if err != nil { - // If we get a retryable error that's related to the quota issue, + // If we get a retryable error that's related to deletion verification, // we can assume the project was likely deleted successfully - if strings.Contains(err.Error(), "Resource Quota PodsPerProject not found") || - strings.Contains(err.Error(), "deletion verification in progress") { + if strings.Contains(err.Error(), "deletion verification in progress") || + strings.Contains(err.Error(), "not found") { // Log a warning but don't fail the deletion // The project deletion was successful, but verification failed due to timing issues return diff --git a/pinecone/provider/projects_data_source.go b/pinecone/provider/projects_data_source.go index 7a23401..4d88c15 100644 --- a/pinecone/provider/projects_data_source.go +++ b/pinecone/provider/projects_data_source.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider import ( diff --git a/pinecone/provider/projects_data_source_test.go b/pinecone/provider/projects_data_source_test.go index 26f24f9..3dd0c73 100644 --- a/pinecone/provider/projects_data_source_test.go +++ b/pinecone/provider/projects_data_source_test.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider import (