Skip to content

Commit

Permalink
feat: redis
Browse files Browse the repository at this point in the history
  • Loading branch information
miton18 committed Feb 18, 2025
1 parent b5c3025 commit e5fd401
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ override.tf.json
.terraformrc
terraform.rc
terraform

.envrc
1 change: 0 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "clevercloud Provider"
subcategory: ""
description: |-
CleverCloud provider allow you to interact with CleverCloud platform.
---
Expand Down
33 changes: 33 additions & 0 deletions docs/resources/redis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "clevercloud_redis Resource - terraform-provider-clevercloud"
subcategory: ""
description: |-
Manage Redis databases
---

# clevercloud_redis (Resource)

Manage Redis databases



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Name of the service
- `plan` (String) Database size and spec

### Optional

- `region` (String) Geographical region where the data will be stored

### Read-Only

- `creation_date` (Number) Date of database creation
- `host` (String) Database host, used to connect to
- `id` (String) Generated unique identifier
- `port` (Number) Database port
- `token` (String) Token to authenticate
14 changes: 12 additions & 2 deletions pkg/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ func LookupAddonProvider(providers []tmp.AddonProvider, providerId string) *tmp.
})
}

func LookupProviderPlan(provider *tmp.AddonProvider, planId string) *tmp.AddonPlan {
func LookupProviderPlan(provider *tmp.AddonProvider, planSlug string) *tmp.AddonPlan {
if provider == nil {
return nil
}

return First(provider.Plans, func(plan tmp.AddonPlan) bool {
return strings.EqualFold(plan.Slug, planId)
return strings.EqualFold(plan.Slug, planSlug)
})
}

func LookupProviderPlanByID(provider *tmp.AddonProvider, planID string) *tmp.AddonPlan {
if provider == nil {
return nil
}

return First(provider.Plans, func(plan tmp.AddonPlan) bool {
return strings.EqualFold(plan.ID, planID)
})
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"go.clever-cloud.com/terraform-provider/pkg/resources/php"
"go.clever-cloud.com/terraform-provider/pkg/resources/postgresql"
"go.clever-cloud.com/terraform-provider/pkg/resources/python"
"go.clever-cloud.com/terraform-provider/pkg/resources/redis"
"go.clever-cloud.com/terraform-provider/pkg/resources/scala"
"go.clever-cloud.com/terraform-provider/pkg/resources/static"
)
Expand All @@ -38,4 +39,5 @@ var Resources = []func() resource.Resource{
static.NewResourceStatic(),
docker.NewResourceDocker,
keycloak.NewResourceKeycloak,
redis.NewResourceRedis,
}
205 changes: 205 additions & 0 deletions pkg/resources/redis/crud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package redis

import (
"context"
"fmt"
"strconv"
"strings"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"go.clever-cloud.com/terraform-provider/pkg"
"go.clever-cloud.com/terraform-provider/pkg/provider"
"go.clever-cloud.com/terraform-provider/pkg/tmp"
)

// Weird behaviour, but TF can ask for a Resource without having configured a Provider (maybe for Meta and Schema)
// So we need to handle the case there is no ProviderData
func (r *ResourceRedis) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
tflog.Debug(ctx, "ResourceRedis.Configure()")

// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

provider, ok := req.ProviderData.(provider.Provider)
if ok {
r.cc = provider.Client()
r.org = provider.Organization()
}
}

// Create a new resource
func (r *ResourceRedis) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
kv := Redis{}

resp.Diagnostics.Append(req.Plan.Get(ctx, &kv)...)
if resp.Diagnostics.HasError() {
return
}

addonsProvidersRes := tmp.GetAddonsProviders(ctx, r.cc)
if addonsProvidersRes.HasError() {
resp.Diagnostics.AddError("failed to get addon providers", addonsProvidersRes.Error().Error())
return
}

addonsProviders := addonsProvidersRes.Payload()
provider := pkg.LookupAddonProvider(*addonsProviders, "redis-addon")
plan := pkg.LookupProviderPlan(provider, kv.Plan.ValueString())
if plan == nil {
resp.Diagnostics.AddError("This plan does not exists", "available plans are: "+strings.Join(pkg.ProviderPlansAsList(provider), ", "))
return
}

addonReq := tmp.AddonRequest{
Name: kv.Name.ValueString(),
Plan: plan.ID,
ProviderID: "redis-addon",
Region: kv.Region.ValueString(),
}

res := tmp.CreateAddon(ctx, r.cc, r.org, addonReq)
if res.HasError() {
resp.Diagnostics.AddError("failed to create addon", res.Error().Error())
return
}

kv.ID = pkg.FromStr(res.Payload().RealID)
kv.CreationDate = pkg.FromI(res.Payload().CreationDate)

resp.Diagnostics.Append(resp.State.Set(ctx, kv)...)
if resp.Diagnostics.HasError() {
return
}

envRes := tmp.GetAddonEnv(ctx, r.cc, r.org, kv.ID.ValueString())
if envRes.HasError() {
resp.Diagnostics.AddError("failed to get Redis connection infos", envRes.Error().Error())
return
}

env := *envRes.Payload()
envAsMap := pkg.Reduce(env, map[string]types.String{}, func(acc map[string]types.String, v tmp.EnvVar) map[string]types.String {
acc[v.Name] = pkg.FromStr(v.Value)
return acc
})
tflog.Info(ctx, "API response", map[string]interface{}{
"payload": fmt.Sprintf("%+v", envAsMap),
})
port, err := strconv.ParseInt(envAsMap["REDIS_PORT"].ValueString(), 10, 64)
if err != nil {
resp.Diagnostics.AddError("invalid port received", "expect REDIS_PORT to be an Integer")
}
kv.Host = envAsMap["REDIS_HOST"]
kv.Port = pkg.FromI(port)
kv.Token = envAsMap["REDIS_PASSWORD"]

resp.Diagnostics.Append(resp.State.Set(ctx, kv)...)
if resp.Diagnostics.HasError() {
return
}
}

// Read resource information
func (r *ResourceRedis) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
tflog.Debug(ctx, "Redis READ", map[string]interface{}{"request": req})

var kv Redis
diags := req.State.Get(ctx, &kv)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

addonKVRes := tmp.GetAddon(ctx, r.cc, r.org, kv.ID.ValueString())
if addonKVRes.IsNotFoundError() {
diags = resp.State.SetAttribute(ctx, path.Root("id"), types.StringUnknown())
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
if addonKVRes.IsNotFoundError() {
resp.State.RemoveResource(ctx)
return
}
if addonKVRes.HasError() {
resp.Diagnostics.AddError("failed to get Redis resource", addonKVRes.Error().Error())
}

addonKV := addonKVRes.Payload()
tflog.Debug(ctx, "redis", map[string]interface{}{"payload": fmt.Sprintf("%+v", addonKV)})

envRes := tmp.GetAddonEnv(ctx, r.cc, r.org, kv.ID.ValueString())
if envRes.HasError() {
resp.Diagnostics.AddError("failed to get Redis connection infos", envRes.Error().Error())
return
}

env := *envRes.Payload()
envAsMap := pkg.Reduce(env, map[string]types.String{}, func(acc map[string]types.String, v tmp.EnvVar) map[string]types.String {
acc[v.Name] = pkg.FromStr(v.Value)
return acc
})
tflog.Info(ctx, "API response", map[string]interface{}{
"payload": fmt.Sprintf("%+v", envAsMap),
})
port, err := strconv.ParseInt(envAsMap["REDIS_PORT"].ValueString(), 10, 64)
if err != nil {
resp.Diagnostics.AddError("invalid port received", "expect REDIS_PORT to be an Integer")
}

kv.Name = pkg.FromStr(addonKV.Name)
kv.Host = envAsMap["REDIS_HOST"]
kv.Plan = pkg.FromStr(addonKV.Plan.Slug)
kv.Port = pkg.FromI(port)
kv.Region = pkg.FromStr(addonKV.Region)
kv.Token = envAsMap["REDIS_PASSWORD"]

diags = resp.State.Set(ctx, kv)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Update resource
func (r *ResourceRedis) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// TODO
}

// Delete resource
func (r *ResourceRedis) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
kv := Redis{}

diags := req.State.Get(ctx, &kv)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, "Redis DELETE", map[string]interface{}{"kv": kv})

res := tmp.DeleteAddon(ctx, r.cc, r.org, kv.ID.ValueString())
if res.IsNotFoundError() {
resp.State.RemoveResource(ctx)
return
}
if res.HasError() {
resp.Diagnostics.AddError("failed to delete addon", res.Error().Error())
return
}

resp.State.RemoveResource(ctx)
}

// Import resource
func (r *ResourceRedis) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Save the import identifier in the id attribute
// and call Read() to fill fields
attr := path.Root("id")
resource.ImportStatePassthroughID(ctx, attr, req, resp)
}
1 change: 1 addition & 0 deletions pkg/resources/redis/doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Manage Redis databases
21 changes: 21 additions & 0 deletions pkg/resources/redis/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package redis

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource"
"go.clever-cloud.dev/client"
)

type ResourceRedis struct {
cc *client.Client
org string
}

func NewResourceRedis() resource.Resource {
return &ResourceRedis{}
}

func (r *ResourceRedis) Metadata(ctx context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) {
res.TypeName = req.ProviderTypeName + "_redis"
}
71 changes: 71 additions & 0 deletions pkg/resources/redis/redis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package redis_test

import (
"context"
_ "embed"
"fmt"
"os"
"regexp"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"go.clever-cloud.com/terraform-provider/pkg/helper"
"go.clever-cloud.com/terraform-provider/pkg/provider/impl"
"go.clever-cloud.com/terraform-provider/pkg/tmp"
"go.clever-cloud.dev/client"
)

var protoV6Provider = map[string]func() (tfprotov6.ProviderServer, error){
"clevercloud": providerserver.NewProtocol6WithError(impl.New("test")()),
}

func TestAccRedis_basic(t *testing.T) {
ctx := context.Background()
rName := fmt.Sprintf("tf-test-redis-%d", time.Now().UnixMilli())
fullName := fmt.Sprintf("clevercloud_redis.%s", rName)
cc := client.New(client.WithAutoOauthConfig())
org := os.Getenv("ORGANISATION")
providerBlock := helper.NewProvider("clevercloud").SetOrganisation(org)
materiakvBlock := helper.NewRessource("clevercloud_redis", rName, helper.SetKeyValues(map[string]any{
"name": rName,
"region": "par",
"plan": "m_mono",
}))

resource.Test(t, resource.TestCase{
PreCheck: func() {
if org == "" {
t.Fatalf("missing ORGANISATION env var")
}
},
ProtoV6ProviderFactories: protoV6Provider,
CheckDestroy: func(state *terraform.State) error {
for _, resource := range state.RootModule().Resources {
res := tmp.GetAddon(ctx, cc, org, resource.Primary.ID)
if res.IsNotFoundError() {
continue
}
if res.HasError() {
return fmt.Errorf("unexpectd error: %s", res.Error().Error())
}

return fmt.Errorf("expect resource '%s' to be deleted: %+v", resource.Primary.ID, res.Payload())
}
return nil
},
Steps: []resource.TestStep{{
ResourceName: rName,
Config: providerBlock.Append(materiakvBlock).String(),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(fullName, "id", regexp.MustCompile(`^redis_.*`)),
resource.TestMatchResourceAttr(fullName, "host", regexp.MustCompile(`^.*.services.clever-cloud.com$`)),
resource.TestCheckResourceAttrSet(fullName, "port"),
resource.TestCheckResourceAttrSet(fullName, "token"),
),
}},
})
}
Loading

0 comments on commit e5fd401

Please sign in to comment.