Skip to content

Commit c70357a

Browse files
committed
tailscale/resource_acl: relax policy validation during plan steps
Some resources like SCIM groups can be created and added as a reference to the policy file in the same run. The eager validation causes otherwise valid runs to fail at the plan step because the references do not exist yet. We are relaxing the plan validation logic to avoid such false positives. The full validation results are still available in debug logs in case this is useful. Note that the validation is done in full during an apply before the policy file is changed so this does not permit a tailnet policy to be in an invalid state. Fixes #546 Signed-off-by: mcoulombe <[email protected]>
1 parent 2a6aff4 commit c70357a

File tree

2 files changed

+73
-1
lines changed

2 files changed

+73
-1
lines changed

tailscale/resource_acl.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ package tailscale
66
import (
77
"context"
88
"fmt"
9+
"regexp"
910
"strings"
1011

1112
"github.com/hashicorp/go-cty/cty"
13+
"github.com/hashicorp/terraform-plugin-log/tflog"
1214
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1315
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1416

@@ -25,6 +27,8 @@ If tests are defined in the ACL (the top-level "tests" section), ACL validation
2527
// TODO: use an exported variable when https://github.com/hashicorp/terraform-plugin-sdk/issues/803 has been addressed.
2628
const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
2729

30+
var syntaxErrorMessage = regexp.MustCompile(`^ACL validation failed: line \d+, column \d+:`)
31+
2832
func resourceACL() *schema.Resource {
2933
return &schema.Resource{
3034
Description: resourceACLDescription,
@@ -42,7 +46,17 @@ func resourceACL() *schema.Resource {
4246
if rd.Get("acl").(string) == "" {
4347
return nil
4448
}
45-
return client.PolicyFile().Validate(ctx, rd.Get("acl").(string))
49+
err := client.PolicyFile().Validate(ctx, rd.Get("acl").(string))
50+
if err != nil && syntaxErrorMessage.MatchString(err.Error()) {
51+
return err
52+
} else if err != nil {
53+
// non-syntax errors are logged but do not fail the plan operation
54+
tflog.Debug(ctx, "ACL validation unsuccessful due to advisory error", map[string]interface{}{"error": err})
55+
return nil
56+
}
57+
58+
tflog.Debug(ctx, "ACL validation successful")
59+
return nil
4660
},
4761
Schema: map[string]*schema.Schema{
4862
"acl": {

tailscale/resource_acl_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"fmt"
1010
"net/http"
11+
"regexp"
1112
"testing"
1213

1314
"github.com/google/go-cmp/cmp"
@@ -292,6 +293,63 @@ func TestAccACL_resetOnDestroy(t *testing.T) {
292293
})
293294
}
294295

296+
func TestAccACLValidation(t *testing.T) {
297+
const resourceName = "tailscale_acl.test_acl_validation"
298+
299+
const testACLInvalidSyntax = `
300+
resource "tailscale_acl" "test_acl" {
301+
acl = <<EOF
302+
{
303+
"grants": [
304+
{
305+
"src": ["group:scim-that-does-not-exist-yet"],
306+
"dst": ["*"],
307+
"ip": ["*"]
308+
},
309+
// ] <- Commented out to create invalid syntax, invalid JSON should fail the plan
310+
}
311+
EOF
312+
}`
313+
const testACLUndefinedReferences = `
314+
resource "tailscale_acl" "test_acl" {
315+
acl = <<EOF
316+
{
317+
"grants": [
318+
{
319+
"src": ["group:scim-that-does-not-exist-yet"], // <- Undefined reference do not fail plan because they could be created in the same run
320+
"dst": ["*"],
321+
"ip": ["*"]
322+
},
323+
]
324+
}
325+
EOF
326+
}`
327+
328+
resource.Test(t, resource.TestCase{
329+
PreCheck: func() { testAccPreCheck(t) },
330+
ProviderFactories: testAccProviderFactories(t),
331+
Steps: []resource.TestStep{
332+
{
333+
ResourceName: resourceName,
334+
Config: testACLInvalidSyntax,
335+
PlanOnly: true,
336+
ExpectError: regexp.MustCompile("Error: ACL is not a valid HuJSON string"),
337+
},
338+
{
339+
ResourceName: resourceName,
340+
Config: testACLUndefinedReferences,
341+
PlanOnly: true,
342+
ExpectNonEmptyPlan: true,
343+
},
344+
{
345+
ResourceName: resourceName,
346+
Config: testACLUndefinedReferences,
347+
ExpectError: regexp.MustCompile("Error: Failed to set ACL"), // Apply should still fail if the pre-requisite resource is not created before the ACLs are applied
348+
},
349+
},
350+
})
351+
}
352+
295353
func checkProperties(expected *tailscale.ACL) func(client *tailscale.Client, rs *terraform.ResourceState) error {
296354
return func(client *tailscale.Client, rs *terraform.ResourceState) error {
297355
actual, err := client.PolicyFile().Get(context.Background())

0 commit comments

Comments
 (0)