Skip to content

Commit

Permalink
feat: implement update and destroy
Browse files Browse the repository at this point in the history
  • Loading branch information
thiskevinwang committed Oct 5, 2023
1 parent 5a1d9f8 commit 8739eb0
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 53 deletions.
134 changes: 86 additions & 48 deletions internal/resources/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package resources
import (
"context"
"fmt"
"time"

"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/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"

Expand All @@ -32,28 +35,10 @@ type indexResource struct {
client services.Pinecone
}

// {
// "database": {
// "name": "test",
// "metric": "cosine",
// "dimension": 1536,
// "replicas": 1,
// "shards": 1,
// "pods": 1
// },
// "status": {
// "waiting": [],
// "crashed": [],
// "host": "test-260030d.svc.us-west4-gcp-free.pinecone.io",
// "port": 433,
// "state": "Ready",
// "ready": true
// }
// }

// indexResourceModel maps the resource schema data.
// - "github.com/hashicorp/terraform-plugin-framework/types"
type indexResourceModel struct {
Id types.String `tfsdk:"id"` // for TF
Name types.String `tfsdk:"name"`
Dimension types.Int64 `tfsdk:"dimension"`
Metric types.String `tfsdk:"metric"`
Expand All @@ -72,6 +57,13 @@ func (r *indexResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
resp.Schema = schema.Schema{
Description: "Manages an index.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Service generated identifier.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the index to be created. The maximum length is 45 characters.",
Required: true,
Expand Down Expand Up @@ -125,11 +117,11 @@ func (r *indexResource) Configure(_ context.Context, req resource.ConfigureReque

// Create a new resource.
func (r *indexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
tflog.Error(ctx, "creating a resource")
var plan indexResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
Expand All @@ -148,25 +140,50 @@ func (r *indexResource) Create(ctx context.Context, req resource.CreateRequest,
)
return
}
// poll the describe index endpoint until the index is ready
// Poll every n seconds
ticker := time.NewTicker(10 * time.Second)
shouldRetry := true
for shouldRetry {
diRes, err := r.client.DescribeIndex(name)
if err != nil {
resp.Diagnostics.AddError(
"Failed to poll index",
fmt.Sprintf("Failed to describe index: %s", err),
)
return
}

if diRes.Status.State == "Ready" || diRes.Status.Ready {
shouldRetry = false
} else {
<-ticker.C // keep polling
}
}

// log the response
tflog.Info(ctx, "CreateIndex OK: %s", map[string]any{"response": *response})

plan.Id = types.StringValue(fmt.Sprintf("%s/%s", r.client.Environment, name))

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
}

// Read resource information.
func (r *indexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Get current state
// Read data from Terraform state
var plan indexResourceModel
resp.Diagnostics.Append(resp.State.Get(ctx, &plan)...)
var state indexResourceModel
resp.Diagnostics.Append(resp.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

// Get fresh state from Pinecone
// Generate API request body from plan
name := plan.Name.ValueString()
name := state.Name.ValueString()
response, err := r.client.DescribeIndex(name)
if err != nil {
resp.Diagnostics.AddError(
Expand All @@ -177,54 +194,75 @@ func (r *indexResource) Read(ctx context.Context, req resource.ReadRequest, resp
}

// log the response
tflog.Info(ctx, "DescribeIndex OK: %s", map[string]any{"response": *response})
tflog.Info(ctx, "DescribeIndex OK", map[string]any{"response": *response})

// Save data into Terraform plan
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
// Set refreshed state
state.Name = types.StringValue(response.Database.Name)
state.Dimension = types.Int64Value(response.Database.Dimension)
state.Metric = types.StringValue(response.Database.Metric)
state.Replicas = types.Int64Value(response.Database.Replicas)
state.Pods = types.Int64Value(response.Database.Pods)

resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

// Update resource information.
func (r *indexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data indexResourceModel
var plan indexResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

if resp.Diagnostics.HasError() {
return
}

// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := r.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update example, got error: %s", err))
// return
// }
indexName := plan.Name.ValueString()

confIdxResp, err := r.client.ConfigureIndex(indexName, &services.ConfigureIndexRequest{})
if err != nil {
resp.Diagnostics.AddError(
"Failed to update index",
fmt.Sprintf("Failed to update index: %s", err),
)
return
}

// log the response
tflog.Info(ctx, "ConfigureIndex OK", map[string]any{"response": *confIdxResp})

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

// Delete resource information.
func (r *indexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data indexResourceModel
var state indexResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
// Read Terraform plan data into the model
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := r.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete example, got error: %s", err))
// return
// }
delIdxResp, err := r.client.DeleteIndex(state.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Failed to delete index",
fmt.Sprintf("Failed to delete index: %s", err),
)
return
}

// log the response
tflog.Info(ctx, "DeleteIndex OK", map[string]any{"response": *delIdxResp})

}

func (r *indexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
tflog.Info(ctx, "Import state", map[string]any{"ID": req.ID})
// Retrieve import ID and save to id attribute
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
142 changes: 137 additions & 5 deletions internal/services/pinecone.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ type Pinecone struct {
Environment string
}

// Create
// create_index
// POST
// https://controller.us-west4-gcp-free.pinecone.io/databases
// This operation creates a Pinecone index. You can use it to specify the measure of similarity, the dimension of vectors to be stored in the index, the numbers of replicas to use, and more.
func (p *Pinecone) CreateIndex(name string, dimension int64, metric string) (*string, error) {
url := fmt.Sprintf("https://controller.%s.pinecone.io/databases", p.Environment)

Expand Down Expand Up @@ -67,8 +70,31 @@ func (p *Pinecone) CreateIndex(name string, dimension int64, metric string) (*st
}
}

// Read
func (p *Pinecone) DescribeIndex(name string) (*string, error) {
type DescribeIndexResponse struct {
Database struct {
Name string `json:"name"`
Metric string `json:"metric"`
Dimension int64 `json:"dimension"`
Replicas int64 `json:"replicas"`
Shards int64 `json:"shards"`
Pods int64 `json:"pods"`
} `json:"database"`
Status struct {
Waiting []interface{} `json:"waiting"`
Crashed []interface{} `json:"crashed"`
Host string `json:"host"`
Port int64 `json:"port"`
// values: Initializing, Ready,
State string `json:"state"`
Ready bool `json:"ready"`
} `json:"status"`
}

// describe_index
// GET
// https://controller.us-west4-gcp-free.pinecone.io/databases/{indexName}
// Get a description of an index.
func (p *Pinecone) DescribeIndex(name string) (*DescribeIndexResponse, error) {
url := fmt.Sprintf("https://controller.%s.pinecone.io/databases/%s", p.Environment, name)

// initialize a request
Expand All @@ -92,13 +118,119 @@ func (p *Pinecone) DescribeIndex(name string) (*string, error) {
return nil, err
}

fmt.Println(string(body))
bodyString := string(body)

switch {
case res.StatusCode < 300: // 2xx
return &bodyString, nil
// unmarshal json to struct
descIndexResponse := &DescribeIndexResponse{}
err := json.Unmarshal(body, descIndexResponse)
if err != nil {
return nil, err
}
return descIndexResponse, nil
default: // non-2xx
return nil, fmt.Errorf("DescribeIndex failed with status code %d and message %q", res.StatusCode, bodyString)
}
}

type ConfigureIndexRequest struct {
// The new pod type for the index. One of s1, p1, or p2 appended with . and one of x1, x2, x4, or x8.
PodType string `json:"pod_type"`
// The desired number of replicas for the index.
Replicas int64 `json:"replicas"`
}

// configure_index
// PATCH
// https://controller.us-west4-gcp-free.pinecone.io/databases/{indexName}
// This operation specifies the pod type and number of replicas for an index. Not supported by projects on the gcp-starter environment.
// (Not supported for free tier)
//
// 202 String - The index has been successfully updated
// 400 String - Bad request,not enough quota.
// 404 String - Index not found.
// 500 String - Internal error. Can be caused by invalid parameters.
func (p *Pinecone) ConfigureIndex(name string, data *ConfigureIndexRequest) (*string, error) {
url := fmt.Sprintf("https://controller.%s.pinecone.io/databases/%s", p.Environment, name)

payloadBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}

// convert byte[] to io.Reader
payload := bytes.NewReader(payloadBytes)

// initialize a request
req, err := http.NewRequest("PATCH", url, payload)
if err != nil {
return nil, err
}

req.Header.Add("accept", "application/json")
req.Header.Add("content-type", "application/json")
req.Header.Add("Api-Key", p.ApiKey)

// fire off the request
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

bodyString := string(body)

switch {
case res.StatusCode < 300: // 2xx
return &bodyString, nil
default: // non-2xx
return nil, fmt.Errorf("ConfigureIndex failed with status code %d and message %q", res.StatusCode, bodyString)
}
}

// delete_index
// DELETE
// https://controller.us-west4-gcp-free.pinecone.io/databases/{indexName}
// This operation deletes an existing index.
//
// 202 String - The index has been successfully deleted
// 404 String - Index not found.
// 500 String - Internal error. Can be caused by invalid parameters.
func (p *Pinecone) DeleteIndex(name string) (*string, error) {
url := fmt.Sprintf("https://controller.%s.pinecone.io/databases/%s", p.Environment, name)

// initialize a request
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return nil, err
}

req.Header.Add("accept", "application/json")
req.Header.Add("Api-Key", p.ApiKey)

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

bodyString := string(body)

switch {
case res.StatusCode < 300: // 2xx
return &bodyString, nil
default: // non-2xx
return nil, fmt.Errorf("DeleteIndex failed with status code %d and message %q", res.StatusCode, bodyString)
}
}
Loading

0 comments on commit 8739eb0

Please sign in to comment.