Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Test
run: |
echo "provider_installation { dev_overrides { \"registry.terraform.io/ably/ably\" = \"$PWD/bin\", } direct {} }" > ~/.terraformrc
TF_ACC=1 go test -v -parallel 4 ./...
TF_ACC=1 go test -timeout 20m -v -parallel 4 ./...
env:
ABLY_ACCOUNT_TOKEN: ${{ secrets.ABLY_ACCOUNT_TOKEN }}
ABLY_URL: 'https://staging-control.ably-dev.net/v1'
57 changes: 35 additions & 22 deletions internal/provider/ingress_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,13 @@ func GetPlanIngressRule(plan AblyIngressRule) (any, diag.Diagnostics) {
// GetIngressRuleResponse maps an API rule response to the ingress rule terraform model.
// Ingress rules use the same generic RuleResponse from the client, with target unmarshalled
// according to the ruleType.
func GetIngressRuleResponse(ablyRule *control.RuleResponse, plan *AblyIngressRule) (AblyIngressRule, diag.Diagnostics) {
func GetIngressRuleResponse(ablyRule *control.RuleResponse, plan *AblyIngressRule, reading bool) (AblyIngressRule, diag.Diagnostics) {
var diags diag.Diagnostics
rc := newReconciler(&diags)
if reading {
rc.forRead()
}

var respTarget any

switch ablyRule.RuleType {
Expand All @@ -80,30 +85,38 @@ func GetIngressRuleResponse(ablyRule *control.RuleResponse, plan *AblyIngressRul
diags.AddError("Error unmarshalling ingress rule target", fmt.Sprintf("Could not unmarshal ingress/mongodb target: %s", err.Error()))
return AblyIngressRule{}, diags
}
var pt *AblyIngressRuleTargetMongo
if p, ok := plan.Target.(*AblyIngressRuleTargetMongo); ok {
pt = p
}
respTarget = &AblyIngressRuleTargetMongo{
Url: types.StringValue(target.URL),
Database: types.StringValue(target.Database),
Collection: types.StringValue(target.Collection),
Pipeline: types.StringValue(target.Pipeline),
FullDocument: types.StringValue(target.FullDocument),
FullDocumentBeforeChange: types.StringValue(target.FullDocumentBeforeChange),
PrimarySite: types.StringValue(target.PrimarySite),
Url: rc.str("target.url", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.Url }), types.StringValue(target.URL), false),
Database: rc.str("target.database", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.Database }), types.StringValue(target.Database), false),
Collection: rc.str("target.collection", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.Collection }), types.StringValue(target.Collection), false),
Pipeline: rc.str("target.pipeline", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.Pipeline }), types.StringValue(target.Pipeline), false),
FullDocument: rc.str("target.full_document", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.FullDocument }), types.StringValue(target.FullDocument), false),
FullDocumentBeforeChange: rc.str("target.full_document_before_change", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.FullDocumentBeforeChange }), types.StringValue(target.FullDocumentBeforeChange), false),
PrimarySite: rc.str("target.primary_site", planStr(pt, func(t *AblyIngressRuleTargetMongo) types.String { return t.PrimarySite }), types.StringValue(target.PrimarySite), false),
}
case "ingress-postgres-outbox":
target, err := unmarshalTarget[control.IngressPostgresOutboxTarget](ablyRule.Target)
if err != nil {
diags.AddError("Error unmarshalling ingress rule target", fmt.Sprintf("Could not unmarshal ingress-postgres-outbox target: %s", err.Error()))
return AblyIngressRule{}, diags
}
var pt *AblyIngressRuleTargetPostgresOutbox
if p, ok := plan.Target.(*AblyIngressRuleTargetPostgresOutbox); ok {
pt = p
}
respTarget = &AblyIngressRuleTargetPostgresOutbox{
Url: types.StringValue(target.URL),
OutboxTableSchema: types.StringValue(target.OutboxTableSchema),
OutboxTableName: types.StringValue(target.OutboxTableName),
NodesTableSchema: types.StringValue(target.NodesTableSchema),
NodesTableName: types.StringValue(target.NodesTableName),
SslMode: types.StringValue(target.SSLMode),
SslRootCert: optStringValue(target.SSLRootCert),
PrimarySite: types.StringValue(target.PrimarySite),
Url: rc.str("target.url", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.Url }), types.StringValue(target.URL), false),
OutboxTableSchema: rc.str("target.outbox_table_schema", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.OutboxTableSchema }), types.StringValue(target.OutboxTableSchema), false),
OutboxTableName: rc.str("target.outbox_table_name", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.OutboxTableName }), types.StringValue(target.OutboxTableName), false),
NodesTableSchema: rc.str("target.nodes_table_schema", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.NodesTableSchema }), types.StringValue(target.NodesTableSchema), false),
NodesTableName: rc.str("target.nodes_table_name", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.NodesTableName }), types.StringValue(target.NodesTableName), false),
SslMode: rc.str("target.ssl_mode", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.SslMode }), types.StringValue(target.SSLMode), false),
SslRootCert: rc.str("target.ssl_root_cert", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.SslRootCert }), optStringValue(target.SSLRootCert), false),
PrimarySite: rc.str("target.primary_site", planStr(pt, func(t *AblyIngressRuleTargetPostgresOutbox) types.String { return t.PrimarySite }), types.StringValue(target.PrimarySite), false),
}
default:
diags.AddError(
Expand All @@ -114,9 +127,9 @@ func GetIngressRuleResponse(ablyRule *control.RuleResponse, plan *AblyIngressRul
}

respRule := AblyIngressRule{
ID: types.StringValue(ablyRule.ID),
AppID: types.StringValue(ablyRule.AppID),
Status: types.StringValue(ablyRule.Status),
ID: rc.str("id", plan.ID, types.StringValue(ablyRule.ID), true),
AppID: rc.str("app_id", plan.AppID, types.StringValue(ablyRule.AppID), false),
Status: rc.str("status", plan.Status, types.StringValue(ablyRule.Status), true),
Target: respTarget,
}

Expand Down Expand Up @@ -188,7 +201,7 @@ func CreateIngressRule[T any](r Rule, ctx context.Context, req resource.CreateRe
return
}

responseValues, respDiags := GetIngressRuleResponse(&rule, &plan)
responseValues, respDiags := GetIngressRuleResponse(&rule, &plan, false)
resp.Diagnostics.Append(respDiags...)
if resp.Diagnostics.HasError() {
return
Expand Down Expand Up @@ -232,7 +245,7 @@ func ReadIngressRule[T any](r Rule, ctx context.Context, req resource.ReadReques
return
}

responseValues, respDiags := GetIngressRuleResponse(&rule, &state)
responseValues, respDiags := GetIngressRuleResponse(&rule, &state, true)
resp.Diagnostics.Append(respDiags...)
if resp.Diagnostics.HasError() {
return
Expand Down Expand Up @@ -278,7 +291,7 @@ func UpdateIngressRule[T any](r Rule, ctx context.Context, req resource.UpdateRe
return
}

responseValues, respDiags := GetIngressRuleResponse(&rule, &plan)
responseValues, respDiags := GetIngressRuleResponse(&rule, &plan, false)
resp.Diagnostics.Append(respDiags...)
if resp.Diagnostics.HasError() {
return
Expand Down
24 changes: 12 additions & 12 deletions internal/provider/modifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ func (m defaultBoolModifier) MarkdownDescription(ctx context.Context) string {
}

func (m defaultBoolModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
if resp.PlanValue.IsUnknown() || req.ConfigValue.IsUnknown() {
if req.ConfigValue.IsUnknown() {
return
}
if !req.ConfigValue.IsNull() || !req.PlanValue.IsNull() {
return
if req.ConfigValue.IsNull() && (resp.PlanValue.IsNull() || resp.PlanValue.IsUnknown()) {
resp.PlanValue = m.value
}
resp.PlanValue = m.value
}

// DefaultBoolAttribute returns a plan modifier that sets a default bool value.
Expand All @@ -52,13 +51,12 @@ func (m defaultInt64Modifier) MarkdownDescription(ctx context.Context) string {
}

func (m defaultInt64Modifier) PlanModifyInt64(_ context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) {
if resp.PlanValue.IsUnknown() || req.ConfigValue.IsUnknown() {
if req.ConfigValue.IsUnknown() {
return
}
if !req.ConfigValue.IsNull() || !req.PlanValue.IsNull() {
return
if req.ConfigValue.IsNull() && (resp.PlanValue.IsNull() || resp.PlanValue.IsUnknown()) {
resp.PlanValue = m.value
}
resp.PlanValue = m.value
}

// DefaultInt64Attribute returns a plan modifier that sets a default int64 value.
Expand All @@ -80,13 +78,15 @@ func (m defaultStringModifier) MarkdownDescription(ctx context.Context) string {
}

func (m defaultStringModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
if resp.PlanValue.IsUnknown() || req.ConfigValue.IsUnknown() {
if req.ConfigValue.IsUnknown() {
return
}
if !req.ConfigValue.IsNull() || !req.PlanValue.IsNull() {
return
// Set default when config is null and the plan doesn't already carry
// a concrete value (e.g. from prior state). This covers both Create
// (PlanValue is Unknown for Computed fields) and Update with null state.
if req.ConfigValue.IsNull() && (resp.PlanValue.IsNull() || resp.PlanValue.IsUnknown()) {
resp.PlanValue = m.value
}
resp.PlanValue = m.value
}

// DefaultStringAttribute returns a plan modifier that sets a default string value.
Expand Down
51 changes: 51 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package provider

import (
"fmt"
"os"
"testing"

Expand All @@ -22,3 +23,53 @@ func testAccPreCheck(t *testing.T) {
t.Fatal("ABLY_ACCOUNT_TOKEN must be set for acceptance tests")
}
}

// tfProvider is the shared Terraform provider configuration block used by
// minimal-config acceptance tests across resource test files.
const tfProvider = `
terraform {
required_providers {
ably = {
source = "registry.terraform.io/ably/ably"
}
}
}
provider "ably" {}
`

// minimalRuleConfig builds an HCL config with only required fields for a rule
// resource. The targetHCL argument is the target block content specific to
// each rule type.
func minimalRuleConfig(appName, resourceType, targetHCL string) string {
return fmt.Sprintf(`%s
resource "ably_app" "app0" {
name = %q
}

resource %q "rule0" {
app_id = ably_app.app0.id

source = {
type = "channel.message"
}

%s
}
`, tfProvider, appName, resourceType, targetHCL)
}

// minimalIngressRuleConfig builds an HCL config with only required fields for
// an ingress rule resource.
func minimalIngressRuleConfig(appName, resourceType, targetHCL string) string {
return fmt.Sprintf(`%s
resource "ably_app" "app0" {
name = %q
}

resource %q "rule0" {
app_id = ably_app.app0.id

%s
}
`, tfProvider, appName, resourceType, targetHCL)
}
Loading
Loading