From 5d9dc9a1bd6e28b92171e0994b41f5988bd344ad Mon Sep 17 00:00:00 2001 From: LeCrabe Date: Wed, 11 Dec 2024 11:23:45 +0100 Subject: [PATCH] feat: metabase fix: Read for tests fix: suggested chances --- docs/resources/metabase.md | 31 ++++ pkg/registry/registry.go | 2 + pkg/resources/metabase/crud.go | 185 ++++++++++++++++++++++++ pkg/resources/metabase/doc.md | 1 + pkg/resources/metabase/metabase.go | 21 +++ pkg/resources/metabase/metabase_test.go | 67 +++++++++ pkg/resources/metabase/schema.go | 44 ++++++ pkg/tmp/addon.go | 25 ++++ 8 files changed, 376 insertions(+) create mode 100644 docs/resources/metabase.md create mode 100644 pkg/resources/metabase/crud.go create mode 100644 pkg/resources/metabase/doc.md create mode 100644 pkg/resources/metabase/metabase.go create mode 100644 pkg/resources/metabase/metabase_test.go create mode 100644 pkg/resources/metabase/schema.go diff --git a/docs/resources/metabase.md b/docs/resources/metabase.md new file mode 100644 index 0000000..e58fc02 --- /dev/null +++ b/docs/resources/metabase.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "clevercloud_metabase Resource - terraform-provider-clevercloud" +subcategory: "" +description: |- + Manage Metabase https://www.metabase.com/ product. +--- + +# clevercloud_metabase (Resource) + +Manage [Metabase](https://www.metabase.com/) product. + + + + +## 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) Metabase host, used to connect to +- `id` (String) Generated unique identifier diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 8defd37..18bb0f1 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -10,6 +10,7 @@ import ( "go.clever-cloud.com/terraform-provider/pkg/resources/java" "go.clever-cloud.com/terraform-provider/pkg/resources/keycloak" "go.clever-cloud.com/terraform-provider/pkg/resources/materiakv" + "go.clever-cloud.com/terraform-provider/pkg/resources/metabase" "go.clever-cloud.com/terraform-provider/pkg/resources/mongodb" "go.clever-cloud.com/terraform-provider/pkg/resources/nodejs" "go.clever-cloud.com/terraform-provider/pkg/resources/php" @@ -27,6 +28,7 @@ var Resources = []func() resource.Resource{ cellar.NewResourceCellar, java.NewResourceJava("war"), materiakv.NewResourceMateriaKV, + metabase.NewResourceMetabase, mongodb.NewResourceMongoDB, nodejs.NewResourceNodeJS, php.NewResourcePHP, diff --git a/pkg/resources/metabase/crud.go b/pkg/resources/metabase/crud.go new file mode 100644 index 0000000..20fdabf --- /dev/null +++ b/pkg/resources/metabase/crud.go @@ -0,0 +1,185 @@ +package metabase + +import ( + "context" + "fmt" + "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 *ResourceMetabase) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + tflog.Debug(ctx, "ResourceMetabase.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 *ResourceMetabase) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + mb := Metabase{} + + resp.Diagnostics.Append(req.Plan.Get(ctx, &mb)...) + 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() + prov := pkg.LookupAddonProvider(*addonsProviders, "metabase") + plan := pkg.LookupProviderPlan(prov, mb.Plan.ValueString()) + if plan == nil || plan.ID == "" { + resp.Diagnostics.AddError("failed to find plan", "expect: "+strings.Join(pkg.ProviderPlansAsList(prov), ", ")+", got: "+mb.Plan.String()) + return + } + + addonReq := tmp.AddonRequest{ + Name: mb.Name.ValueString(), + Plan: plan.ID, + ProviderID: "metabase", + Region: mb.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 + } + + mb.ID = pkg.FromStr(res.Payload().ID) + mb.CreationDate = pkg.FromI(res.Payload().CreationDate) + // mb.Plan = pkg.FromStr(res.Payload().Plan.Slug) + + resp.Diagnostics.Append(resp.State.Set(ctx, mb)...) + if resp.Diagnostics.HasError() { + return + } + + mbInfoRes := tmp.GetAddonEnv(ctx, r.cc, r.org, mb.ID.ValueString()) + if mbInfoRes.HasError() { + resp.Diagnostics.AddError("failed to get Metabase connection infos", mbInfoRes.Error().Error()) + return + } + + addonMB := *mbInfoRes.Payload() + tflog.Debug(ctx, "API response", map[string]interface{}{ + "payload": fmt.Sprintf("%+v", addonMB), + }) + + hostEnvVar := pkg.First(addonMB, func(v tmp.EnvVar) bool { + return v.Name == "METABASE_URL" + }) + if hostEnvVar == nil { + resp.Diagnostics.AddError("cannot get Metabase infos", "missing METABASE_URL env var on created addon") + return + } + + mb.Host = pkg.FromStr(hostEnvVar.Value) + + resp.Diagnostics.Append(resp.State.Set(ctx, mb)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read resource information +func (r *ResourceMetabase) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Debug(ctx, "Metabase READ", map[string]interface{}{"request": req}) + + var mb Metabase + diags := req.State.Get(ctx, &mb) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + addonMBRes := tmp.GetMetabase(ctx, r.cc, mb.ID.ValueString()) + if addonMBRes.IsNotFoundError() { + diags = resp.State.SetAttribute(ctx, path.Root("id"), types.StringUnknown()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + // if addonMBRes.IsNotFoundError() { + // resp.State.RemoveResource(ctx) + // return + // } + // if addonMBRes.HasError() { + // resp.Diagnostics.AddError("failed to get Metabase resource", addonMBRes.Error().Error()) + // } + + addonMB := addonMBRes.Payload() + + if addonMB.Status == "TO_DELETE" { + resp.State.RemoveResource(ctx) + return + } + tflog.Debug(ctx, "STATE", map[string]interface{}{"mb": mb}) + tflog.Debug(ctx, "API", map[string]interface{}{"mb": addonMB}) + // mb.Host = pkg.FromStr(addonMB.Applications[0].Host) + // mb.Port = pkg.FromI(int64(addonMB.Port)) + // mb.User = pkg.FromStr(addonMB.User) + // mb.Password = pkg.FromStr(addonMB.Password) + + diags = resp.State.Set(ctx, mb) + resp.Diagnostics.Append(diags...) +} + +// Update resource +func (r *ResourceMetabase) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // TODO +} + +// Delete resource +func (r *ResourceMetabase) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var mb Metabase + + diags := req.State.Get(ctx, &mb) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Debug(ctx, "Metabase DELETE", map[string]interface{}{"mb": mb}) + + res := tmp.DeleteAddon(ctx, r.cc, r.org, mb.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 *ResourceMetabase) 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) +} diff --git a/pkg/resources/metabase/doc.md b/pkg/resources/metabase/doc.md new file mode 100644 index 0000000..2c2ea5d --- /dev/null +++ b/pkg/resources/metabase/doc.md @@ -0,0 +1 @@ +Manage [Metabase](https://www.metabase.com/) product. diff --git a/pkg/resources/metabase/metabase.go b/pkg/resources/metabase/metabase.go new file mode 100644 index 0000000..70c8d12 --- /dev/null +++ b/pkg/resources/metabase/metabase.go @@ -0,0 +1,21 @@ +package metabase + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "go.clever-cloud.dev/client" +) + +type ResourceMetabase struct { + cc *client.Client + org string +} + +func NewResourceMetabase() resource.Resource { + return &ResourceMetabase{} +} + +func (r *ResourceMetabase) Metadata(ctx context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) { + res.TypeName = req.ProviderTypeName + "_metabase" +} diff --git a/pkg/resources/metabase/metabase_test.go b/pkg/resources/metabase/metabase_test.go new file mode 100644 index 0000000..d5a29ce --- /dev/null +++ b/pkg/resources/metabase/metabase_test.go @@ -0,0 +1,67 @@ +package metabase_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 TestAccMetabase_basic(t *testing.T) { + ctx := context.Background() + rName := fmt.Sprintf("tf-test-mb-%d", time.Now().UnixMilli()) + fullName := fmt.Sprintf("clevercloud_metabase.%s", rName) + cc := client.New(client.WithAutoOauthConfig()) + org := os.Getenv("ORGANISATION") + providerBlock := helper.NewProvider("clevercloud").SetOrganisation(org).String() + metabaseBlock := helper.NewRessource("clevercloud_metabase", rName, helper.SetKeyValues(map[string]any{"name": rName, "plan": "beta", "region": "par"})).String() + + 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.GetMetabase(ctx, cc, resource.Primary.ID) + if res.IsNotFoundError() { + continue + } + if res.HasError() { + return fmt.Errorf("unexpectd error: %s", res.Error().Error()) + } + if res.Payload().Status == "TO_DELETE" { + continue + } + + return fmt.Errorf("expect resource '%s' to be deleted", resource.Primary.ID) + } + return nil + }, + Steps: []resource.TestStep{{ + ResourceName: rName, + Config: providerBlock + metabaseBlock, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr(fullName, "id", regexp.MustCompile(`^addon_.*`)), + ), + }}, + }) +} diff --git a/pkg/resources/metabase/schema.go b/pkg/resources/metabase/schema.go new file mode 100644 index 0000000..dbfc2fc --- /dev/null +++ b/pkg/resources/metabase/schema.go @@ -0,0 +1,44 @@ +package metabase + +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" + "go.clever-cloud.com/terraform-provider/pkg/attributes" +) + +type Metabase struct { + attributes.Addon + Host types.String `tfsdk:"host"` + // HerokuId types.String `tfsdk:"heroku_id"` + // CallbackURL types.String `tfsdk:"callback_url"` + // LogplexToken types.String `tfsdk:"logplex_token"` + // OwnerId types.String `tfsdk:"owner_id"` +} + +//go:embed doc.md +var resourceMetabaseDoc string + +func (r ResourceMetabase) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 0, + MarkdownDescription: resourceMetabaseDoc, + Attributes: attributes.WithAddonCommons(map[string]schema.Attribute{ + // customer provided + // TODO: Markdown description + "host": schema.StringAttribute{Computed: true, MarkdownDescription: "Metabase host, used to connect to"}, + // "heroku_id": schema.StringAttribute{Computed: true, MarkdownDescription: "heroku_id"}, + // "callback_url": schema.StringAttribute{Computed: true, MarkdownDescription: "callback_url"}, + // "logplex_token": schema.StringAttribute{Computed: true, MarkdownDescription: "logplex_token"}, + // "owner_id": schema.StringAttribute{Computed: true, MarkdownDescription: "owner_id"}, + }), + } +} + +// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade#implementing-state-upgrade-support +func (r ResourceMetabase) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{} +} diff --git a/pkg/tmp/addon.go b/pkg/tmp/addon.go index 81e3c26..7a14681 100644 --- a/pkg/tmp/addon.go +++ b/pkg/tmp/addon.go @@ -94,6 +94,31 @@ func GetMateriaKV(ctx context.Context, cc *client.Client, organisationID, postgr return client.Get[MateriaKV](ctx, cc, path) } +type Metabase struct { + OwnerID string `json:"ownerId"` + ID string `json:"addonId"` + NetworkgroupID *string `json:"networkgroupId"` + PostgresID string `json:"postgresId"` + Status string `json:"status" example:"ACTIVE"` + Applications []MetabaseApplication `json:"applications"` +} +type MetabaseApplication struct { + MetabaseID string `json:"appId"` + MetabasePlan string `json:"planIdentifier"` + Host string `json:"host"` + JavaApplicationID string `json:"javaId"` +} + +func CreateMetabase(ctx context.Context, cc *client.Client, organisation string, addon AddonRequest) client.Response[AddonResponse] { + path := "/v2/providers/addon-metabase/resources" + return client.Post[AddonResponse](ctx, cc, path, addon) +} + +func GetMetabase(ctx context.Context, cc *client.Client, metabaseID string) client.Response[Metabase] { + path := fmt.Sprintf("/v4/addon-providers/addon-metabase/addons/%s", metabaseID) + return client.Get[Metabase](ctx, cc, path) +} + type MongoDB struct { Host string `json:"host"` Port int64 `json:"port"`