Skip to content

Commit

Permalink
Support hujson for ACL files
Browse files Browse the repository at this point in the history
This commit swaps all uses of `encoding/json` with `tailscale/hujson`. This allows users of the provider
to have a hujson file locally that contains all the features such as comments and trailing commas.

When the ACL is pushed to tailscale via the provider, the comments will be lost when encoding/decoding
is done into the `ACL` type. However, the source of truth for the ACL will be local along with the terraform
configuration files, so this should not be a problem.

Closes #17

Signed-off-by: David Bond <davidsbond93@gmail.com>
  • Loading branch information
davidsbond committed Aug 30, 2021
1 parent b2fa62c commit 4c19d8e
Show file tree
Hide file tree
Showing 23 changed files with 5,527 additions and 40 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88 h1:q5Sxx79nhG4xWsYEJBlLdqo1hNhUV31/NhA4qQ1SKAY=
github.com/tailscale/hujson v0.0.0-20210818175511-7360507a6e88/go.mod h1:iTDXJsA6A2wNNjurgic2rk+is6uzU4U2NLm4T+edr6M=
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand Down
31 changes: 16 additions & 15 deletions internal/tailscale/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"

"github.com/tailscale/hujson"
)

type (
Expand Down Expand Up @@ -51,7 +52,7 @@ func (c *Client) buildRequest(ctx context.Context, method, uri string, body inte

var bodyBytes []byte
if body != nil {
bodyBytes, err = json.MarshalIndent(body, "", " ")
bodyBytes, err = hujson.MarshalIndent(body, "", " ")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -82,7 +83,7 @@ func (c *Client) performRequest(req *http.Request, out interface{}) error {

if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
var apiErr APIError
if err = json.NewDecoder(res.Body).Decode(&apiErr); err != nil {
if err = hujson.NewDecoder(res.Body).Decode(&apiErr); err != nil {
return err
}

Expand All @@ -91,7 +92,7 @@ func (c *Client) performRequest(req *http.Request, out interface{}) error {
}

if out != nil {
return json.NewDecoder(res.Body).Decode(out)
return hujson.NewDecoder(res.Body).Decode(out)
}

return nil
Expand Down Expand Up @@ -165,23 +166,23 @@ func (c *Client) DNSNameservers(ctx context.Context) ([]string, error) {
}

type ACL struct {
ACLs []ACLEntry `json:"acls"`
Groups map[string][]string `json:"groups,omitempty"`
Hosts map[string]string `json:"hosts,omitempty"`
TagOwners map[string][]string `json:"tagowners,omitempty"`
Tests []ACLTest `json:"tests,omitempty"`
ACLs []ACLEntry `json:"acls" hujson:"ACLs,omitempty"`
Groups map[string][]string `json:"groups,omitempty" hujson:"Groups,omitempty"`
Hosts map[string]string `json:"hosts,omitempty" hujson:"Hosts,omitempty"`
TagOwners map[string][]string `json:"tagowners,omitempty" hujson:"TagOwners,omitempty"`
Tests []ACLTest `json:"tests,omitempty" hujson:"Tests,omitempty"`
}

type ACLEntry struct {
Action string `json:"action"`
Ports []string `json:"ports"`
Users []string `json:"users"`
Action string `json:"action" hujson:"Action"`
Ports []string `json:"ports" hujson:"Ports"`
Users []string `json:"users" hujson:"Users"`
}

type ACLTest struct {
User string `json:"user"`
Allow []string `json:"allow"`
Deny []string `json:"deny"`
User string `json:"user" hujson:"User"`
Allow []string `json:"allow" hujson:"Allow"`
Deny []string `json:"deny" hujson:"Deny"`
}

// ACL retrieves the ACL that is currently set for the given tailnet.
Expand Down
86 changes: 85 additions & 1 deletion internal/tailscale/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,93 @@ import (

"github.com/davidsbond/terraform-provider-tailscale/internal/tailscale"
"github.com/google/go-cmp/cmp"
"github.com/tailscale/hujson"
)

func TestDomainACL_Unmarshal(t *testing.T) {
func TestDomainACL_HuJSON_Unmarshal(t *testing.T) {
acl := `
{
// Allow all users access to all ports.
"ACLS": [
{
"Action": "accept",
"Users": ["*"],
"Ports": ["*:*"]
}
],
"TagOwners": {
"tag:example": [
"group:example",
]
},
"Groups": {
"group:example": [
"user1@example.com",
"user2@example.com",
]
},
"Hosts": {
"example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24",
},
"Tests": [
{
"User": "user1@example.com",
"Allow": ["example-host-1:22", "example-host-2:80"],
"Deny": ["exapmle-host-2:100"],
},
{
"User": "user2@example.com",
"Allow": ["100.60.3.4:22"],
}
]
}`

var actual tailscale.ACL
if err := hujson.Unmarshal([]byte(acl), &actual); err != nil {
t.Fatal(err)
}

expected := tailscale.ACL{
ACLs: []tailscale.ACLEntry{
{
Action: "accept",
Ports: []string{"*:*"},
Users: []string{"*"},
},
},
TagOwners: map[string][]string{
"tag:example": {"group:example"},
},
Hosts: map[string]string{
"example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24",
},
Groups: map[string][]string{
"group:example": {
"user1@example.com",
"user2@example.com",
},
},
Tests: []tailscale.ACLTest{
{
User: "user1@example.com",
Allow: []string{"example-host-1:22", "example-host-2:80"},
Deny: []string{"exapmle-host-2:100"},
},
{
User: "user2@example.com",
Allow: []string{"100.60.3.4:22"},
},
},
}

if !cmp.Equal(expected, actual) {
t.Fatal("unmarshalled ACL does not match expected value")
}
}

func TestDomainACL_JSON_Unmarshal(t *testing.T) {
acl := `
{
"acls": [
Expand Down
7 changes: 7 additions & 0 deletions tailscale/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"

"github.com/davidsbond/terraform-provider-tailscale/internal/tailscale"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -60,6 +61,12 @@ func diagnosticsError(err error, message string, args ...interface{}) diag.Diagn
}
}

func diagnosticsErrorWithPath(err error, message string, path cty.Path, args ...interface{}) diag.Diagnostics {
d := diagnosticsError(err, message, args...)
d[0].AttributePath = path
return d
}

func createUUID() string {
val, err := uuid.GenerateUUID()
if err != nil {
Expand Down
21 changes: 10 additions & 11 deletions tailscale/resource_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ package tailscale

import (
"context"
"encoding/json"

"github.com/davidsbond/terraform-provider-tailscale/internal/tailscale"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/tailscale/hujson"
)

func resourceACL() *schema.Resource {
Expand All @@ -30,10 +29,10 @@ func resourceACL() *schema.Resource {
}
}

func validateACL(i interface{}, _ cty.Path) diag.Diagnostics {
func validateACL(i interface{}, p cty.Path) diag.Diagnostics {
var acl tailscale.ACL
if err := json.Unmarshal([]byte(i.(string)), &acl); err != nil {
return diagnosticsError(err, "Invalid ACL")
if err := hujson.Unmarshal([]byte(i.(string)), &acl); err != nil {
return diagnosticsErrorWithPath(err, "Invalid ACL", p)
}
return nil
}
Expand All @@ -42,15 +41,15 @@ func suppressACLDiff(_, old, new string, _ *schema.ResourceData) bool {
var oldACL tailscale.ACL
var newACL tailscale.ACL

if err := json.Unmarshal([]byte(old), &oldACL); err != nil {
if err := hujson.Unmarshal([]byte(old), &oldACL); err != nil {
return false
}

if err := json.Unmarshal([]byte(new), &newACL); err != nil {
if err := hujson.Unmarshal([]byte(new), &newACL); err != nil {
return false
}

return cmp.Equal(oldACL, newACL)
return oldACL.Equal(newACL)
}

func resourceACLRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
Expand All @@ -60,7 +59,7 @@ func resourceACLRead(ctx context.Context, d *schema.ResourceData, m interface{})
return diagnosticsError(err, "Failed to fetch ACL")
}

aclStr, err := json.MarshalIndent(acl, "", " ")
aclStr, err := hujson.MarshalIndent(acl, "", " ")
if err != nil {
return diagnosticsError(err, "Failed to marshal ACL for")
}
Expand All @@ -83,7 +82,7 @@ func resourceACLCreate(ctx context.Context, d *schema.ResourceData, m interface{
aclStr := d.Get("acl").(string)

var acl tailscale.ACL
if err := json.Unmarshal([]byte(aclStr), &acl); err != nil {
if err := hujson.Unmarshal([]byte(aclStr), &acl); err != nil {
return diagnosticsError(err, "Failed to unmarshal ACL")
}

Expand All @@ -104,7 +103,7 @@ func resourceACLUpdate(ctx context.Context, d *schema.ResourceData, m interface{
}

var acl tailscale.ACL
if err := json.Unmarshal([]byte(aclStr), &acl); err != nil {
if err := hujson.Unmarshal([]byte(aclStr), &acl); err != nil {
return diagnosticsError(err, "Failed to unmarshal ACL")
}

Expand Down
29 changes: 16 additions & 13 deletions tailscale/resource_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,40 @@ const testACL = `
resource "tailscale_acl" "test_acl" {
acl = <<EOF
{
"acls": [
// Access control lists.
"ACLs": [
{
"action": "accept",
"users": ["*"],
"ports": ["*:*"]
"Action": "accept",
"Users": ["*"],
"Ports": ["*:*"]
}
],
"tagowners": {
"TagOwners": {
"tag:example": [
"group:example"
]
},
"groups": {
// Declare static groups of users
"Groups": {
"group:example": [
"user1@example.com",
"user2@example.com"
]
},
"hosts": {
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"example-host-1": "100.100.100.100",
"example-host-2": "100.100.101.100/24"
},
"tests": [
"Tests": [
{
"user": "user1@example.com",
"allow": ["example-host-1:22", "example-host-2:80"],
"deny": ["exapmle-host-2:100"]
"User": "user1@example.com",
"Allow": ["example-host-1:22", "example-host-2:80"],
"Deny": ["exapmle-host-2:100"]
},
{
"user": "user2@example.com",
"allow": ["100.60.3.4:22"]
"User": "user2@example.com",
"Allow": ["100.60.3.4:22"]
}
]
}
Expand Down
29 changes: 29 additions & 0 deletions vendor/github.com/tailscale/hujson/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4c19d8e

Please sign in to comment.