Skip to content
Draft
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
16 changes: 15 additions & 1 deletion tailscale/resource_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package tailscale
import (
"context"
"fmt"
"regexp"
"strings"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

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

var syntaxErrorMessage = regexp.MustCompile(`^ACL validation failed: line \d+, column \d+:`)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe include a comment about what assumptions this makes about the control server.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean by assumptions about the control server?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The assumption that all syntax errors present as an error in this form, and that if this change, the logic breaks.


func resourceACL() *schema.Resource {
return &schema.Resource{
Description: resourceACLDescription,
Expand All @@ -42,7 +46,17 @@ func resourceACL() *schema.Resource {
if rd.Get("acl").(string) == "" {
return nil
}
return client.PolicyFile().Validate(ctx, rd.Get("acl").(string))
err := client.PolicyFile().Validate(ctx, rd.Get("acl").(string))
if err != nil && syntaxErrorMessage.MatchString(err.Error()) {
return err
} else if err != nil {
// non-syntax errors are logged but do not fail the plan operation
tflog.Debug(ctx, "ACL validation unsuccessful due to advisory error", map[string]interface{}{"error": err})
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
tflog.Debug(ctx, "ACL validation unsuccessful due to advisory error", map[string]interface{}{"error": err})
tflog.Debug(ctx, "ACL validation unsuccessful due to advisory warning, apply may fail if dependencies are unmet", map[string]interface{}{"error": err})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can soften the wording and call them warnings, but the validation failures are not necessarily undefined dependencies. The provider should not characterize what caused the error if it just forwards what the BE returned, which can change in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

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

are not necessarily

Hence the use of "may"

return nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From our conversations, warnings might be counterproductive and confuse people, but we should not make the information completely unavailable.


tflog.Debug(ctx, "ACL validation successful")
return nil
},
Schema: map[string]*schema.Schema{
"acl": {
Expand Down
58 changes: 58 additions & 0 deletions tailscale/resource_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -292,6 +293,63 @@ func TestAccACL_resetOnDestroy(t *testing.T) {
})
}

func TestAccACLValidation(t *testing.T) {
const resourceName = "tailscale_acl.test_acl_validation"

const testACLInvalidSyntax = `
resource "tailscale_acl" "test_acl" {
acl = <<EOF
{
"grants": [
{
"src": ["group:scim-that-does-not-exist-yet"],
"dst": ["*"],
"ip": ["*"]
},
// ] <- Commented out to create invalid syntax, invalid JSON should fail the plan
}
EOF
}`
const testACLUndefinedReferences = `
resource "tailscale_acl" "test_acl" {
acl = <<EOF
{
"grants": [
{
"src": ["group:scim-that-does-not-exist-yet"], // <- Undefined reference do not fail plan because they could be created in the same run
"dst": ["*"],
"ip": ["*"]
},
]
}
EOF
}`

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: testAccProviderFactories(t),
Steps: []resource.TestStep{
{
ResourceName: resourceName,
Config: testACLInvalidSyntax,
PlanOnly: true,
ExpectError: regexp.MustCompile("Error: ACL is not a valid HuJSON string"),
},
{
ResourceName: resourceName,
Config: testACLUndefinedReferences,
PlanOnly: true,
ExpectNonEmptyPlan: true,
},
{
ResourceName: resourceName,
Config: testACLUndefinedReferences,
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
},
},
})
}

func checkProperties(expected *tailscale.ACL) func(client *tailscale.Client, rs *terraform.ResourceState) error {
return func(client *tailscale.Client, rs *terraform.ResourceState) error {
actual, err := client.PolicyFile().Get(context.Background())
Expand Down
Loading