This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/terraform-provider-paimon.git
The following commit(s) were added to refs/heads/main by this push:
new 53f428c Plan no changes after importing row filters and column masks
(#35)
53f428c is described below
commit 53f428c8aad43ded50283c3144ed8d2bc9c41775
Author: Jiajia Li <[email protected]>
AuthorDate: Sat Oct 10 15:11:13 2026 +0800
Plan no changes after importing row filters and column masks (#35)
---
docs/resources/column_mask.md | 5 ++
docs/resources/row_filter.md | 5 ++
internal/provider/helpers.go | 16 ++++++
internal/provider/helpers_test.go | 14 +++++
internal/provider/management_acceptance_test.go | 76 +++++++++++++++++++++++++
internal/provider/resource_policy.go | 7 ++-
6 files changed, 121 insertions(+), 2 deletions(-)
diff --git a/docs/resources/column_mask.md b/docs/resources/column_mask.md
index 646a008..e73f088 100644
--- a/docs/resources/column_mask.md
+++ b/docs/resources/column_mask.md
@@ -105,6 +105,11 @@ terraform import paimon_column_mask.analyst_email \
'database=analytics&table=events&principal=role%3Aanalyst&column=email'
```
+Import records `allow_non_atomic_update` as its default `false` and stores the
+server's `transform` in `jsonencode` form (sorted keys, no whitespace), so
+adopting a policy whose configuration omits the flag plans no changes. When the
+configuration sets the flag to `true`, the next apply only records it in state.
+
The provider compares known Java policy AST semantics across create, refresh,
and update: field references resolve by name against the current table schema,
and one-child AND/OR predicates simplify to their child. Equivalent configured
diff --git a/docs/resources/row_filter.md b/docs/resources/row_filter.md
index 009dafd..85e30c7 100644
--- a/docs/resources/row_filter.md
+++ b/docs/resources/row_filter.md
@@ -109,6 +109,11 @@ terraform import paimon_row_filter.analyst_region \
'database=analytics&table=events&principal=role%3Aanalyst'
```
+Import records `allow_non_atomic_update` as its default `false` and stores the
+server's `predicate` in `jsonencode` form (sorted keys, no whitespace), so
+adopting a policy whose configuration omits the flag plans no changes. When the
+configuration sets the flag to `true`, the next apply only records it in state.
+
The provider compares known Java policy AST semantics across create, refresh,
and update: field references resolve by name against the current table schema,
and one-child AND/OR predicates simplify to their child. Equivalent configured
diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go
index 405f99b..0141d3f 100644
--- a/internal/provider/helpers.go
+++ b/internal/provider/helpers.go
@@ -123,6 +123,22 @@ func equivalentJSON(left, right string) bool {
return reflect.DeepEqual(leftValue, rightValue)
}
+// compactJSON formats JSON like jsonencode, avoiding whitespace-only diffs.
+func compactJSON(value string) string {
+ var decoded any
+ decoder := json.NewDecoder(strings.NewReader(value))
+ decoder.UseNumber()
+ if decoder.Decode(&decoded) != nil || !json.Valid([]byte(value)) {
+ return value
+ }
+ encoded, err := json.Marshal(decoded)
+ if err != nil {
+ return value
+ }
+
+ return string(encoded)
+}
+
func mutationRecoveryContext(ctx context.Context, api *client.Client)
(context.Context, context.CancelFunc) {
// A canceled apply context can still follow a mutation that committed
remotely.
// Keep request-scoped values, but give reconciliation and repair a
fresh bound.
diff --git a/internal/provider/helpers_test.go
b/internal/provider/helpers_test.go
index 182aa7d..d3c285d 100644
--- a/internal/provider/helpers_test.go
+++ b/internal/provider/helpers_test.go
@@ -62,3 +62,17 @@ func TestRetryLookupUntilReturnsLastStaleStateAtDeadline(t
*testing.T) {
assert.False(t, converged)
assert.Equal(t, "stale", value)
}
+
+func TestCompactJSONMatchesJSONEncode(t *testing.T) {
+ for name, tc := range map[string]struct{ in, want string }{
+ "whitespace": {"{\n \"name\": \"NULL\"\n}",
`{"name":"NULL"}`},
+ "key order": {`{"name":"CONCAT","inputs":["***"]}`,
`{"inputs":["***"],"name":"CONCAT"}`},
+ "large integer": {`{"literals":[9007199254740993]}`,
`{"literals":[9007199254740993]}`},
+ "html escaping": {`{"literals":["a<b&c>d"]}`,
`{"literals":["a\u003cb\u0026c\u003ed"]}`},
+ "invalid": {`{"name":`, `{"name":`},
+ } {
+ t.Run(name, func(t *testing.T) {
+ assert.Equal(t, tc.want, compactJSON(tc.in))
+ })
+ }
+}
diff --git a/internal/provider/management_acceptance_test.go
b/internal/provider/management_acceptance_test.go
index 41ad155..9053ef9 100644
--- a/internal/provider/management_acceptance_test.go
+++ b/internal/provider/management_acceptance_test.go
@@ -16,6 +16,7 @@
package provider
import (
+ "bytes"
"encoding/json"
"fmt"
"net/http"
@@ -198,3 +199,78 @@ func TestAccManagementResourceLifecycle(t *testing.T) {
require.Empty(t, catalog.policies)
require.Empty(t, catalog.permissions)
}
+
+func TestAccPolicyImportPlansNoChanges(t *testing.T) {
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("set TF_ACC=1 to run Terraform acceptance tests")
+ }
+ // Some servers return indented policy JSON.
+ indent := func(value string) string {
+ var out bytes.Buffer
+ require.NoError(t, json.Indent(&out, []byte(value), "", " "))
+
+ return out.String()
+ }
+ catalog := &managementAcceptanceCatalog{policies:
make(map[string]client.DataPolicy), canonicalize: func(p client.PolicyRequest)
client.PolicyRequest {
+ if p.RowFilter != nil {
+ p.RowFilter = &client.RowFilter{Predicate:
indent(p.RowFilter.Predicate)}
+ }
+ if p.ColumnMask != nil {
+ p.ColumnMask = &client.ColumnMask{OnColumn:
p.ColumnMask.OnColumn, Transform: indent(p.ColumnMask.Transform)}
+ }
+
+ return p
+ }}
+ server := httptest.NewServer(catalog)
+ defer server.Close()
+ config := fmt.Sprintf("provider \"paimon\" { uri = %q }\n", server.URL)
+ managementTableConfig("analytics") + `
+resource "paimon_row_filter" "region" {
+ database = paimon_table.events.database
+ table = paimon_table.events.name
+ principal = "role:analyst"
+ predicate = jsonencode({
+ kind = "LEAF"
+ transform = { name = "FIELD_REF", fieldRef = { index = 1, name = "tenant",
type = "STRING" } }
+ function = "EQUAL"
+ literals = ["APAC"]
+ })
+}
+resource "paimon_column_mask" "secret" {
+ database = paimon_table.events.database
+ table = paimon_table.events.name
+ principal = "role:analyst"
+ column = "secret"
+ transform = jsonencode({ name = "NULL" })
+}
+`
+ // Forget the policies without dropping them.
+ forget := fmt.Sprintf("provider \"paimon\" { uri = %q }\n", server.URL)
+ managementTableConfig("analytics") + `
+removed {
+ from = paimon_row_filter.region
+ lifecycle { destroy = false }
+}
+removed {
+ from = paimon_column_mask.secret
+ lifecycle { destroy = false }
+}
+`
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {Config: config},
+ {Config: forget},
+ {
+ Config: config, ResourceName:
"paimon_row_filter.region", ImportState: true, ImportStatePersist: true,
+ ImportStateId:
"database=analytics&table=events&principal=role%3Aanalyst",
+ },
+ {
+ Config: config, ResourceName:
"paimon_column_mask.secret", ImportState: true, ImportStatePersist: true,
+ ImportStateId:
"database=analytics&table=events&principal=role%3Aanalyst&column=secret",
+ },
+ {Config: config, PlanOnly: true},
+ },
+ })
+ catalog.mu.Lock()
+ defer catalog.mu.Unlock()
+ require.Empty(t, catalog.policies)
+}
diff --git a/internal/provider/resource_policy.go
b/internal/provider/resource_policy.go
index cf21f3b..9969608 100644
--- a/internal/provider/resource_policy.go
+++ b/internal/provider/resource_policy.go
@@ -313,6 +313,8 @@ func (r *rowFilterResource) ImportState(ctx
context.Context, req resource.Import
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("database"), model.Database)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("table"), model.Table)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("principal"), model.Principal)...)
+ // The server does not store this flag; import its default.
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("allow_non_atomic_update"), false)...)
}
func (r *rowFilterResource) readIntoState(ctx context.Context, model
*rowFilterResourceModel, diags *diag.Diagnostics) bool {
@@ -333,7 +335,7 @@ func (r *rowFilterResource) readIntoState(ctx
context.Context, model *rowFilterR
return false
}
if model.Predicate.IsNull() || model.Predicate.IsUnknown() || !equal {
- model.Predicate = types.StringValue(remote)
+ model.Predicate = types.StringValue(compactJSON(remote))
}
model.ID = types.StringValue(rowFilterID(*model))
@@ -505,6 +507,7 @@ func (r *columnMaskResource) ImportState(ctx
context.Context, req resource.Impor
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("table"), model.Table)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("principal"), model.Principal)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("column"), model.Column)...)
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx,
path.Root("allow_non_atomic_update"), false)...)
}
func (r *columnMaskResource) readIntoState(ctx context.Context, model
*columnMaskResourceModel, diags *diag.Diagnostics) bool {
@@ -525,7 +528,7 @@ func (r *columnMaskResource) readIntoState(ctx
context.Context, model *columnMas
return false
}
if model.Transform.IsNull() || model.Transform.IsUnknown() || !equal {
- model.Transform = types.StringValue(remote)
+ model.Transform = types.StringValue(compactJSON(remote))
}
model.ID = types.StringValue(columnMaskID(*model))