Skip to content

Commit

Permalink
Merge pull request #14 from mittwald/refact/resources
Browse files Browse the repository at this point in the history
Refactor resource implementations
  • Loading branch information
martin-helmich authored Dec 15, 2023
2 parents 8faeeaf + daed722 commit 0f92215
Show file tree
Hide file tree
Showing 22 changed files with 776 additions and 305 deletions.
3 changes: 2 additions & 1 deletion api/mittwaldv2/client_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ type AppClient interface {
GetAppInstallation(ctx context.Context, appInstallationID string) (*DeMittwaldV1AppAppInstallation, error)
WaitUntilAppInstallationIsReady(ctx context.Context, appID string) error
UninstallApp(ctx context.Context, appInstallationID string) error
LinkAppInstallationToDatabase(ctx context.Context, appInstallationID string, databaseID string, purpose AppLinkDatabaseJSONBodyPurpose) error
LinkAppInstallationToDatabase(ctx context.Context, appInstallationID string, databaseID string, userID string, purpose AppLinkDatabaseJSONBodyPurpose) error
UnlinkAppInstallationFromDatabase(ctx context.Context, appInstallationID string, databaseID string) error
GetSystemSoftwareByName(ctx context.Context, name string) (*DeMittwaldV1AppSystemSoftware, bool, error)
SelectSystemSoftwareVersion(ctx context.Context, systemSoftwareID, versionSelector string) (DeMittwaldV1AppSystemSoftwareVersionSet, error)
GetSystemSoftwareAndVersion(ctx context.Context, systemSoftwareID, systemSoftwareVersionID string) (*DeMittwaldV1AppSystemSoftware, *DeMittwaldV1AppSystemSoftwareVersion, error)
Expand Down
23 changes: 21 additions & 2 deletions api/mittwaldv2/client_app_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,17 @@ func (c *appClient) LinkAppInstallationToDatabase(
ctx context.Context,
appInstallationID string,
databaseID string,
userID string,
purpose AppLinkDatabaseJSONBodyPurpose,
) error {
userIDs := map[string]string{
"admin": userID,
}

response, err := c.client.AppLinkDatabaseWithResponse(ctx, uuid.MustParse(appInstallationID), AppLinkDatabaseJSONRequestBody{
DatabaseId: uuid.MustParse(databaseID),
Purpose: purpose,
DatabaseId: uuid.MustParse(databaseID),
Purpose: purpose,
DatabaseUserIds: &userIDs,
})
if err != nil {
return err
Expand All @@ -123,3 +129,16 @@ func (c *appClient) LinkAppInstallationToDatabase(

return errUnexpectedStatus(response.StatusCode(), response.Body)
}

func (c *appClient) UnlinkAppInstallationFromDatabase(ctx context.Context, appInstallationID string, databaseID string) error {
resp, err := c.client.AppUnlinkDatabaseWithResponse(ctx, uuid.MustParse(appInstallationID), uuid.MustParse(databaseID))
if err != nil {
return err
}

if resp.StatusCode() >= 200 && resp.StatusCode() < 300 {
return nil
}

return errUnexpectedStatus(resp.StatusCode(), resp.Body)
}
18 changes: 18 additions & 0 deletions api/mittwaldv2/client_app_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,25 @@ func UpdateAppInstallationSystemSoftware(systemSoftwareID, systemSoftwareVersion
})
}

func RemoveAppInstallationSystemSoftware(systemSoftwareID string) AppInstallationUpdater {
return AppInstallationUpdaterFunc(func(b *AppPatchAppinstallationJSONRequestBody) {
if b.SystemSoftware == nil {
systemSoftware := make(AppPatchInstallationSystemSoftware)
b.SystemSoftware = &systemSoftware
}

(*b.SystemSoftware)[systemSoftwareID] = AppPatchInstallationSystemSoftwareItem{
SystemSoftwareVersion: nil,
UpdatePolicy: nil,
}
})
}

func (c *appClient) UpdateAppInstallation(ctx context.Context, appInstallationID string, updater ...AppInstallationUpdater) error {
if len(updater) == 0 {
return nil
}

body := AppPatchAppinstallationJSONRequestBody{}

for _, u := range updater {
Expand Down
51 changes: 51 additions & 0 deletions api/mittwaldv2/client_opts.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package mittwaldv2

import (
"bytes"
"context"
"github.com/hashicorp/terraform-plugin-log/tflog"
"io"
"net/http"
)

type ClientBuilderOption func(*clientBuilder, *Client)

func WithAPIToken(token string) ClientBuilderOption {
Expand All @@ -13,3 +21,46 @@ func WithEndpoint(endpoint string) ClientBuilderOption {
c.Server = endpoint
}
}

type debuggingClient struct {
HttpRequestDoer
withRequestBodies bool
}

func (c *debuggingClient) Do(req *http.Request) (*http.Response, error) {
logFields := map[string]any{
"method": req.Method,
"url": req.URL.String(),
}

if req.Body != nil && c.withRequestBodies {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}

req.Body = io.NopCloser(bytes.NewBuffer(body))
logFields["body"] = string(body)
}

res, err := c.HttpRequestDoer.Do(req)

if res != nil {
logFields["status"] = res.StatusCode
}

if err != nil {
logFields["err"] = err
}

tflog.Debug(context.Background(), "executed request", logFields)

return res, err
}

func WithDebugging(withRequestBodies bool) ClientBuilderOption {
return func(_ *clientBuilder, c *Client) {
originalClient := c.Client
c.Client = &debuggingClient{HttpRequestDoer: originalClient, withRequestBodies: withRequestBodies}
}
}
13 changes: 11 additions & 2 deletions api/mittwaldv2/poll.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package mittwaldv2
import (
"context"
"errors"
"github.com/hashicorp/terraform-plugin-log/tflog"
"math"
"time"
)

Expand All @@ -12,7 +14,8 @@ func poll[T any](ctx context.Context, f func() (T, error)) (T, error) {
res := make(chan T)
err := make(chan error)

t := time.NewTicker(200 * time.Millisecond)
d := 100 * time.Millisecond
t := time.NewTicker(d)

defer func() {
t.Stop()
Expand All @@ -26,12 +29,17 @@ func poll[T any](ctx context.Context, f func() (T, error)) (T, error) {
return
}

d = time.Duration(math.Max(float64(d)*1.1, float64(10*time.Second)))
t.Reset(d)

r, e := f()
if e != nil {
if notFound := (ErrNotFound{}); errors.As(e, &notFound) {
continue
} else if permissionDenied := (ErrPermissionDenied{}); errors.As(e, &permissionDenied) {
continue
} else if errors.Is(e, context.DeadlineExceeded) {
return
} else {
err <- e
return
Expand All @@ -45,10 +53,11 @@ func poll[T any](ctx context.Context, f func() (T, error)) (T, error) {

select {
case <-ctx.Done():
return null, ctx.Err()
return null, ErrNotFound{}
case r := <-res:
return r, nil
case e := <-err:
tflog.Debug(ctx, "polling failed", map[string]any{"error": e})
return null, e
}
}
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ provider "mittwald" {
### Optional

- `api_key` (String, Sensitive) API key for the Mittwald API; if omitted, the `MITTWALD_API_TOKEN` environment variable will be used.
- `debug_request_bodies` (Boolean) Whether to log request bodies when debugging is enabled. CAUTION: This will log sensitive data such as passwords in plain text!
- `endpoint` (String) API endpoint for the Mittwald API. Default to `https://api.mittwald.de/v2` if omitted.
25 changes: 22 additions & 3 deletions docs/resources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ resource "mittwald_app" "wordpress" {
}
resource "mittwald_app" "custom_php" {
project_id = mittwald_project.foobar.id
database_id = mittwald_mysql_database.foobar_database.id
project_id = mittwald_project.foobar.id
app = "php"
version = "1.0.0"
Expand All @@ -62,6 +61,15 @@ resource "mittwald_app" "custom_php" {
document_root = "/public"
update_policy = "none"
databases = [
{
kind = "mysql"
purpose = "primary"
id = mittwald_mysql_database.foobar_database.id
user_id = mittwald_mysql_database.foobar_database.user.id
}
]
dependencies = {
(data.mittwald_systemsoftware.php.name) = {
version = data.mittwald_systemsoftware.php.version
Expand Down Expand Up @@ -91,7 +99,7 @@ resource "mittwald_app" "custom_php" {

### Optional

- `database_id` (String) The ID of the database the app uses
- `databases` (Attributes Set) The databases the app uses (see [below for nested schema](#nestedatt--databases))
- `dependencies` (Attributes Map) The dependencies of the app (see [below for nested schema](#nestedatt--dependencies))
- `description` (String) The description of the app
- `document_root` (String) The document root of the app
Expand All @@ -103,6 +111,17 @@ resource "mittwald_app" "custom_php" {
- `installation_path` (String) The installation path of the app
- `version_current` (String) The current version of the app

<a id="nestedatt--databases"></a>
### Nested Schema for `databases`

Required:

- `id` (String) The ID of the database
- `kind` (String) The kind of the database; one of `mysql` or `redis`
- `purpose` (String) The purpose of the database; use 'primary' for the primary data storage, or 'cache' for a cache database
- `user_id` (String) The ID of the database user that the app should use


<a id="nestedatt--dependencies"></a>
### Nested Schema for `dependencies`

Expand Down
12 changes: 10 additions & 2 deletions examples/resources/mittwald_app/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ resource "mittwald_app" "wordpress" {
}

resource "mittwald_app" "custom_php" {
project_id = mittwald_project.foobar.id
database_id = mittwald_mysql_database.foobar_database.id
project_id = mittwald_project.foobar.id

app = "php"
version = "1.0.0"
Expand All @@ -47,6 +46,15 @@ resource "mittwald_app" "custom_php" {
document_root = "/public"
update_policy = "none"

databases = [
{
kind = "mysql"
purpose = "primary"
id = mittwald_mysql_database.foobar_database.id
user_id = mittwald_mysql_database.foobar_database.user.id
}
]

dependencies = {
(data.mittwald_systemsoftware.php.name) = {
version = data.mittwald_systemsoftware.php.version
Expand Down
11 changes: 9 additions & 2 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ type MittwaldProvider struct {

// MittwaldProviderModel describes the provider data model.
type MittwaldProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
APIKey types.String `tfsdk:"api_key"`
Endpoint types.String `tfsdk:"endpoint"`
APIKey types.String `tfsdk:"api_key"`
DebugRequestBodies types.Bool `tfsdk:"debug_request_bodies"`
}

func (p *MittwaldProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
Expand All @@ -53,6 +54,10 @@ func (p *MittwaldProvider) Schema(_ context.Context, _ provider.SchemaRequest, r
Optional: true,
Sensitive: true,
},
"debug_request_bodies": schema.BoolAttribute{
MarkdownDescription: "Whether to log request bodies when debugging is enabled. CAUTION: This will log sensitive data such as passwords in plain text!",
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -87,6 +92,8 @@ func (p *MittwaldProvider) Configure(ctx context.Context, req provider.Configure
opts = append(opts, mittwaldv2.WithEndpoint(data.Endpoint.ValueString()))
}

opts = append(opts, mittwaldv2.WithDebugging(data.DebugRequestBodies.ValueBool()))

client := mittwaldv2.New(opts...)

resp.DataSourceData = client
Expand Down
24 changes: 20 additions & 4 deletions internal/provider/providerutil/err_diag.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package providerutil

import "github.com/hashicorp/terraform-plugin-framework/diag"
import (
"errors"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/mittwald/terraform-provider-mittwald/api/mittwaldv2"
)

func ErrorValueToDiag[T any](res T, err error) func(d *diag.Diagnostics, summary string) T {
return func(d *diag.Diagnostics, summary string) T {
Expand All @@ -20,23 +24,35 @@ func ErrorToDiag(err error) func(d *diag.Diagnostics, summary string) {
}

type WrappedError[T any] struct {
diag *diag.Diagnostics
summary string
diag *diag.Diagnostics
summary string
ignoreNotFound bool
}

func (w *WrappedError[T]) Do(err error) {
if err != nil {
if notFound := (mittwaldv2.ErrNotFound{}); errors.As(err, &notFound) && w.ignoreNotFound {
return
}
if permissionDenied := (mittwaldv2.ErrPermissionDenied{}); errors.As(err, &permissionDenied) && w.ignoreNotFound {
return
}
w.diag.AddError(w.summary, err.Error())
}
}

func (w *WrappedError[T]) IgnoreNotFound() *WrappedError[T] {
w.ignoreNotFound = true
return w
}

func (w *WrappedError[T]) DoVal(res T, err error) T {
w.Do(err)
return res
}

func Try[T any](d *diag.Diagnostics, summary string) *WrappedError[T] {
return &WrappedError[T]{d, summary}
return &WrappedError[T]{diag: d, summary: summary}
}

func EmbedDiag[T any](resultValue T, resultDiag diag.Diagnostics) func(outDiag *diag.Diagnostics) T {
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/resource/appresource/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "github.com/hashicorp/terraform-plugin-framework/types"
type ResourceModel struct {
ID types.String `tfsdk:"id"`
ProjectID types.String `tfsdk:"project_id"`
DatabaseID types.String `tfsdk:"database_id"` // TODO: There may theoretically be multiple database links
Databases types.Set `tfsdk:"databases"`
Description types.String `tfsdk:"description"`
App types.String `tfsdk:"app"`
Version types.String `tfsdk:"version"`
Expand Down
Loading

0 comments on commit 0f92215

Please sign in to comment.