Skip to content

feat: add dedicated database resources - #34

Open
levivannoort wants to merge 1 commit into
betafrom
feat/dedicated-databases
Open

feat: add dedicated database resources#34
levivannoort wants to merge 1 commit into
betafrom
feat/dedicated-databases

Conversation

@levivannoort

Copy link
Copy Markdown
Member

Onboards Appwrite dedicated databases (managed PostgreSQL, MySQL, and Mongo instances) onto the provider.

Resources

  • appwrite_dedicated_database — CRUD + import for a managed database instance, engine chosen via an engine attribute (postgresql / mysql / mongo, ForceNew). Waits for async provisioning to finish so connection_string / hostname / etc. are populated; connection password and URI are marked sensitive.
  • appwrite_dedicated_database_backup_policy — CRUD + import for a database's backup policy.

Design notes

  • One resource with an engine selector. The three engines share byte-identical create/update params, response models, and REST paths — only the /postgresql|/mysql|/mongo URL segment differs. Rather than triplicate ~30 typed option-builders per method, this package issues raw engine-keyed REST calls and decodes into the shared SDK models, mirroring the existing GetColumnRaw helper.
  • SDK wiring is temporary and quarantined. Built against the unreleased dedicated-databases SDK, which currently lives on a fork with a different module path (github.com/aw-tests/sdk-for-go/v6) that self-references that path — so a replace can't stand in for the real path. The fork is therefore required additionally and used only by this new package (which builds its own client from the raw provider credentials); no existing imports change. Flip this package's import prefix back to appwrite/sdk-for-go and drop the fork require once the feature ships in the official SDK (noted in the package doc comment). This is why the PR targets a beta base branch rather than main.

Out of scope (follow-ups)

Day-2 imperative ops that don't fit Terraform's declarative model — failover, migration, in-place upgrade, restorations, branches, SQL executions, connection pooler, PG extensions — and update-only tuning knobs (sql_api_*, metrics_*).

Verification

  • make lint + make build + package unit tests pass.
  • terraform providers schema -json boots the provider and passes the framework's schema validation for both resources.
  • Docs generated via make docs with zero churn to existing pages.
  • Not run: acceptance tests (TF_ACC=1) — need a live Appwrite server with the feature enabled.

🤖 Generated with Claude Code

Onboards Appwrite dedicated databases (managed PostgreSQL, MySQL, and
Mongo instances) onto the provider:

- appwrite_dedicated_database: CRUD + import, waits for async
  provisioning so connection details are populated, exposes connection
  and status/sizing fields (password/URI marked sensitive).
- appwrite_dedicated_database_backup_policy: CRUD + import.

A single resource selects the engine via an `engine` attribute. The
three engines share byte-identical params, models, and REST paths
(differing only in the URL segment), so this package issues raw
engine-keyed REST calls and decodes into the shared SDK models, mirroring
the existing GetColumnRaw helper rather than triplicating typed option
builders.

Built against the unreleased dedicated-databases SDK, which currently
lives on a fork with a different module path. To avoid a module-identity
conflict the fork is required additionally and used only by this new
package (which builds its own client from the raw provider credentials);
no existing imports change. Swap the import prefix back to
appwrite/sdk-for-go and drop the fork require once the feature ships in
the official SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Terraform support for Appwrite dedicated databases. The main changes are:

  • New appwrite_dedicated_database resource with raw engine-keyed CRUD calls.
  • New appwrite_dedicated_database_backup_policy resource.
  • Provider registration and raw credential fields for the forked SDK client.
  • Generated docs and examples for both resources.

Confidence Score: 4/5

The async database lifecycle paths need fixes before merging.

  • A created database can be left outside Terraform state when readiness polling fails.
  • Updates can expose transitional connection fields to downstream resources.
  • Terminal non-ready statuses can turn into long generic timeouts.

internal/services/dedicateddatabase/resource.go; internal/services/dedicateddatabase/helpers.go

Important Files Changed

Filename Overview
internal/services/dedicateddatabase/resource.go Adds the dedicated database resource, including schema, CRUD, import, state mapping, and async create waiting.
internal/services/dedicateddatabase/helpers.go Adds raw API helpers, fork SDK client construction, import parsing, and readiness polling.
internal/services/dedicateddatabase/backup_policy_resource.go Adds backup policy schema, CRUD, import parsing, and state mapping.
internal/provider/provider.go Registers the two new resources and passes raw provider configuration to resource clients.
internal/common/helpers.go Extends shared provider clients with raw endpoint, API key, self-signed, and user-agent values.
go.mod Adds the temporary forked Appwrite SDK dependency used by the dedicated database package.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
internal/services/dedicateddatabase/resource.go:212-219
**Created Database Loses State**

When the POST succeeds but readiness polling later times out, is canceled, or hits a transient read error, this returns before writing the created database ID to state. Terraform then forgets the remote database it just created, so the next apply can create another dedicated database instead of reconciling the first one.

### Issue 2 of 4
internal/services/dedicateddatabase/resource.go:293-305
**Update Stores Transitional State**

Changing fields such as `specification`, `replicas`, storage autoscaling, or PITR can start asynchronous reconfiguration, but this path writes the immediate PATCH response directly to state. If the API returns `scaling` or another transitional response with stale or empty connection details, downstream Terraform references can receive unusable `hostname` or `connection_string` values until a later refresh.

### Issue 3 of 4
internal/services/dedicateddatabase/helpers.go:126-136
**Terminal Statuses Keep Polling**

The wait loop only treats `failed` and `deleted` as terminal errors, so any other terminal non-ready status keeps polling until the full timeout. If the API reports a state such as `error` or `unhealthy`, Terraform waits up to 30 minutes and then reports a generic timeout instead of failing on the actual database state.

### Issue 4 of 4
internal/services/dedicateddatabase/backup_policy_resource.go:254-265
**Slash IDs Shift Import Parts**

The import parser accepts extra slashes inside the middle segment because each call to `splitTwo` only splits once. An import like `postgresql/db/with/slash/policy` becomes `database_id = "db"` and `policy_id = "with/slash/policy"`, so the later read builds a different policy path than the user intended and can import the wrong object or fail with a misleading not-found error.

Reviews (1): Last reviewed commit: "feat: add dedicated database resources" | Re-trigger Greptile

Comment on lines +212 to +219

// Provisioning is asynchronous; wait so connection details are populated.
if err := waitForDatabaseReady(ctx, func() (string, error) {
var cur models.DedicatedDatabase
if err := apiCall(c, r.clients.UserAgent, "GET", "/"+engine+"/"+db.Id, nil, &cur); err != nil {
return "", err
}
return cur.Status, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Created Database Loses State

When the POST succeeds but readiness polling later times out, is canceled, or hits a transient read error, this returns before writing the created database ID to state. Terraform then forgets the remote database it just created, so the next apply can create another dedicated database instead of reconciling the first one.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/services/dedicateddatabase/resource.go
Line: 212-219

Comment:
**Created Database Loses State**

When the POST succeeds but readiness polling later times out, is canceled, or hits a transient read error, this returns before writing the created database ID to state. Terraform then forgets the remote database it just created, so the next apply can create another dedicated database instead of reconciling the first one.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +293 to +305
params["networkIPAllowlist"] = allow
}
if resp.Diagnostics.HasError() {
return
}

var db models.DedicatedDatabase
if err := apiCall(c, r.clients.UserAgent, "PATCH", "/"+engine+"/"+plan.ID.ValueString(), params, &db); err != nil {
resp.Diagnostics.AddError("Error updating dedicated database", common.FormatError(err))
return
}

plan.ProjectID = types.StringValue(projectID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Update Stores Transitional State

Changing fields such as specification, replicas, storage autoscaling, or PITR can start asynchronous reconfiguration, but this path writes the immediate PATCH response directly to state. If the API returns scaling or another transitional response with stale or empty connection details, downstream Terraform references can receive unusable hostname or connection_string values until a later refresh.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/services/dedicateddatabase/resource.go
Line: 293-305

Comment:
**Update Stores Transitional State**

Changing fields such as `specification`, `replicas`, storage autoscaling, or PITR can start asynchronous reconfiguration, but this path writes the immediate PATCH response directly to state. If the API returns `scaling` or another transitional response with stale or empty connection details, downstream Terraform references can receive unusable `hostname` or `connection_string` values until a later refresh.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +126 to +136
if err != nil {
return fmt.Errorf("error checking database %q status: %w", databaseID, err)
}
switch status {
case "ready", "inactive", "paused":
return nil
case "failed":
return fmt.Errorf("database %q entered failed state", databaseID)
case "deleted":
return fmt.Errorf("database %q was deleted during provisioning", databaseID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Terminal Statuses Keep Polling

The wait loop only treats failed and deleted as terminal errors, so any other terminal non-ready status keeps polling until the full timeout. If the API reports a state such as error or unhealthy, Terraform waits up to 30 minutes and then reports a generic timeout instead of failing on the actual database state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/services/dedicateddatabase/helpers.go
Line: 126-136

Comment:
**Terminal Statuses Keep Polling**

The wait loop only treats `failed` and `deleted` as terminal errors, so any other terminal non-ready status keeps polling until the full timeout. If the API reports a state such as `error` or `unhealthy`, Terraform waits up to 30 minutes and then reports a generic timeout instead of failing on the actual database state.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +254 to +265
func (r *backupPolicyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
engine, rest, ok := splitTwo(req.ID)
if ok {
var dbID, policyID string
dbID, policyID, ok = splitTwo(rest)
if ok {
if _, valid := engines[engine]; !valid {
resp.Diagnostics.AddError("Invalid engine", fmt.Sprintf("Engine must be one of: %s", validEngines()))
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("engine"), engine)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), dbID)...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Slash IDs Shift Import Parts

The import parser accepts extra slashes inside the middle segment because each call to splitTwo only splits once. An import like postgresql/db/with/slash/policy becomes database_id = "db" and policy_id = "with/slash/policy", so the later read builds a different policy path than the user intended and can import the wrong object or fail with a misleading not-found error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/services/dedicateddatabase/backup_policy_resource.go
Line: 254-265

Comment:
**Slash IDs Shift Import Parts**

The import parser accepts extra slashes inside the middle segment because each call to `splitTwo` only splits once. An import like `postgresql/db/with/slash/policy` becomes `database_id = "db"` and `policy_id = "with/slash/policy"`, so the later read builds a different policy path than the user intended and can import the wrong object or fail with a misleading not-found error.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant