Skip to content

Commit

Permalink
feat(addon): generic addons support (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
miton18 authored Dec 12, 2023
1 parent b1e10bb commit 939e9c8
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 33 deletions.
38 changes: 38 additions & 0 deletions docs/resources/addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "clevercloud_addon Resource - terraform-provider-clevercloud"
subcategory: ""
description: |-
Manage any addon through the addon-provider API https://developers.clever-cloud.com/doc/extend/add-ons-api/#add-on-provider-api
List of available providers:
Mailpace https://mailpace.com/
---

# clevercloud_addon (Resource)

Manage any addon through the [addon-provider API](https://developers.clever-cloud.com/doc/extend/add-ons-api/#add-on-provider-api)


List of available providers:

* [Mailpace](https://mailpace.com/)



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

### Required

- `name` (String) Name of the addon
- `plan` (String) billing plan
- `region` (String) Geographical region where the addon will be deployed (when relevant)
- `third_party_provider` (String) Provider ID

### Read-Only

- `configurations` (Map of String, Sensitive) Any configuration exposed by the addon
- `creation_date` (Number) Date of database creation
- `id` (String) Generated unique identifier


31 changes: 31 additions & 0 deletions pkg/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package pkg

import "go.clever-cloud.com/terraform-provider/pkg/tmp"

func AddonProvidersAsList(providers []tmp.AddonProvider) []string {
return Map(providers, func(provider tmp.AddonProvider) string {
return provider.ID
})
}

func LookupAddonProvider(providers []tmp.AddonProvider, providerId string) *tmp.AddonProvider {
return First(providers, func(provider tmp.AddonProvider) bool {
return provider.ID == providerId
})
}

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

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

func ProviderPlansAsList(provider *tmp.AddonProvider) []string {
return Map(provider.Plans, func(plan tmp.AddonPlan) string {
return plan.Slug
})
}
2 changes: 1 addition & 1 deletion pkg/provider/impl/provider.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CleverCloud provider allow you to interract with CleverCloud platform.
CleverCloud provider allow you to interact with CleverCloud platform.
4 changes: 3 additions & 1 deletion pkg/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package registry
import (
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/resource"
"go.clever-cloud.com/terraform-provider/pkg/resources/addon"
"go.clever-cloud.com/terraform-provider/pkg/resources/cellar"
"go.clever-cloud.com/terraform-provider/pkg/resources/cellar/bucket"
"go.clever-cloud.com/terraform-provider/pkg/resources/java"
Expand All @@ -18,9 +19,10 @@ var Datasources = []func() datasource.DataSource{}
var Resources = []func() resource.Resource{
cellar.NewResourceCellar,
bucket.NewResourceCellarBucket,
addon.NewResourceAddon,
postgresql.NewResourcePostgreSQL,
nodejs.NewResourceNodeJS,
php.NewResourcePHP,
postgresql.NewResourcePostgreSQL,
java.NewResourceJava("war"),
scala.NewResourceScala(),
static.NewResourceStatic(),
Expand Down
3 changes: 3 additions & 0 deletions pkg/resources/addon/provider_test_block.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
provider "clevercloud" {
organisation = "%s"
}
21 changes: 21 additions & 0 deletions pkg/resources/addon/resource_addon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package addon

import (
"context"

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

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

func NewResourceAddon() resource.Resource {
return &ResourceAddon{}
}

func (r *ResourceAddon) Metadata(ctx context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) {
res.TypeName = req.ProviderTypeName + "_addon"
}
6 changes: 6 additions & 0 deletions pkg/resources/addon/resource_addon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Manage any addon through the [addon-provider API](https://developers.clever-cloud.com/doc/extend/add-ons-api/#add-on-provider-api)


List of available providers:

* [Mailpace](https://mailpace.com/)
188 changes: 188 additions & 0 deletions pkg/resources/addon/resource_addon_crud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package addon

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/attr"
"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 *ResourceAddon) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
tflog.Info(ctx, "ResourceAddon.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 *ResourceAddon) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
ad := Addon{}

resp.Diagnostics.Append(req.Plan.Get(ctx, &ad)...)
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, ad.ThirdPartyProvider.ValueString())
if provider == nil {
resp.Diagnostics.AddError("This provider does not exists", fmt.Sprintf("available providers are: %s", strings.Join(pkg.AddonProvidersAsList(*addonsProviders), ", ")))
return
}

plan := pkg.LookupProviderPlan(provider, ad.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: ad.Name.ValueString(),
Plan: plan.ID,
ProviderID: provider.ID,
Region: ad.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
}

ad.ID = pkg.FromStr(res.Payload().ID)
ad.CreationDate = pkg.FromI(res.Payload().CreationDate)

envRes := tmp.GetAddonEnv(ctx, r.cc, r.org, res.Payload().ID)
if res.HasError() {
resp.Diagnostics.AddError("failed to get addon env", res.Error().Error())
return
}

envAsMap := pkg.Reduce(*envRes.Payload(), map[string]attr.Value{}, func(acc map[string]attr.Value, v tmp.EnvVar) map[string]attr.Value {
acc[v.Name] = pkg.FromStr(v.Value)
return acc
})
ad.Configurations = types.MapValueMust(types.StringType, envAsMap)

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

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

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

addonRes := tmp.GetAddon(ctx, r.cc, r.org, ad.ID.ValueString())
if addonRes.HasError() {
resp.Diagnostics.AddError("failed to get addon", addonRes.Error().Error())
return
}

addonEnvRes := tmp.GetAddonEnv(ctx, r.cc, r.org, ad.ID.ValueString())
if addonEnvRes.HasError() {
resp.Diagnostics.AddError("failed to get addon env", addonEnvRes.Error().Error())
return
}

envAsMap := pkg.Reduce(*addonEnvRes.Payload(), map[string]attr.Value{}, func(acc map[string]attr.Value, v tmp.EnvVar) map[string]attr.Value {
acc[v.Name] = pkg.FromStr(v.Value)
return acc
})

a := addonRes.Payload()
ad.Name = pkg.FromStr(a.Name)
ad.Plan = pkg.FromStr(a.Plan.Slug)
ad.Region = pkg.FromStr(a.Region)
ad.ThirdPartyProvider = pkg.FromStr(a.Provider.ID)
ad.CreationDate = pkg.FromI(a.CreationDate)
ad.Configurations = types.MapValueMust(types.StringType, envAsMap)

/*addonPGRes := tmp.GetPostgreSQL(ctx, r.cc, pg.ID.ValueString())
if addonPGRes.IsNotFoundError() {
diags = resp.State.SetAttribute(ctx, path.Root("id"), types.StringUnknown())
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
if addonPGRes.HasError() {
resp.Diagnostics.AddError("failed to get Postgres resource", addonPGRes.Error().Error())
}*/

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

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

// Delete resource
func (r *ResourceAddon) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var ad Addon

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

res := tmp.DeleteAddon(ctx, r.cc, r.org, ad.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 *ResourceAddon) 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)
}
52 changes: 52 additions & 0 deletions pkg/resources/addon/resource_addon_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package addon

import (
"context"
_ "embed"

"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type Addon struct {
ID types.String `tfsdk:"id"`
ThirdPartyProvider types.String `tfsdk:"third_party_provider"`
Name types.String `tfsdk:"name"`
CreationDate types.Int64 `tfsdk:"creation_date"`
Plan types.String `tfsdk:"plan"`
Region types.String `tfsdk:"region"`
Configurations types.Map `tfsdk:"configurations"`
}

//go:embed resource_addon.md
var resourcePostgresqlDoc string

func (r ResourceAddon) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Version: 0,
MarkdownDescription: resourcePostgresqlDoc,
Attributes: map[string]schema.Attribute{
// customer provided
"name": schema.StringAttribute{Required: true, MarkdownDescription: "Name of the addon"},
"plan": schema.StringAttribute{Required: true, MarkdownDescription: "billing plan"},
"region": schema.StringAttribute{Required: true, MarkdownDescription: "Geographical region where the addon will be deployed (when relevant)"},
"third_party_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Provider ID"},

// provider
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "Generated unique identifier"},
"creation_date": schema.Int64Attribute{Computed: true, MarkdownDescription: "Date of database creation"},
"configurations": schema.MapAttribute{
Computed: true,
Sensitive: true,
MarkdownDescription: "Any configuration exposed by the addon",
ElementType: types.StringType,
},
},
}
}

// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade#implementing-state-upgrade-support
func (r ResourceAddon) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{}
}
Loading

0 comments on commit 939e9c8

Please sign in to comment.