diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 14cb824..08cad98 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -60,6 +60,8 @@ jobs:
image: zookeeper:3.5
ports:
- 2181:2181
+ env:
+ ZOO_MAX_CLIENT_CNXNS: 200
strategy:
fail-fast: false
matrix:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd0ab2f..a921cfa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
IMPROVEMENTS:
* Enabling CI testing for versions `1.9` of Terraform
+* Added support for digest authentication in provider configuration
+* Added support for ZNode ACL management in `zookeeper_znode` and `zookeeper_sequential_znode` resources
NOTES:
diff --git a/README.md b/README.md
index a7c3af6..4fbc5be 100644
--- a/README.md
+++ b/README.md
@@ -37,8 +37,8 @@ workflow.
## Provider features
* [x] support for ZK standard multi-server connection string
-* [ ] support for ZK authentication
-* [ ] support for ZK ACLs
+* [x] support for ZK authentication
+* [x] support for ZK ACLs
* [x] "session timeout" configuration
* [x] create ZNode
* [x] create Sequential ZNode
diff --git a/docs/data-sources/znode.md b/docs/data-sources/znode.md
index 445e240..ee1d17a 100644
--- a/docs/data-sources/znode.md
+++ b/docs/data-sources/znode.md
@@ -44,11 +44,22 @@ output "best_team_znode_data_base64" {
### Read-Only
+- `acl` (List of Object) List of ACL entries for the ZNode. (see [below for nested schema](#nestedatt--acl))
- `data` (String) Content of the ZNode. Use this if content is a UTF-8 string.
- `data_base64` (String) Content of the ZNode, encoded in Base64. Use this if content is binary (i.e. sequence of bytes).
- `id` (String) The ID of this resource.
- `stat` (List of Object) [ZooKeeper Stat Structure](https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_zkStatStructure) of the ZNode. More details about `stat` can be found [here](../../docs#the-stat-structure). (see [below for nested schema](#nestedatt--stat))
+
+### Nested Schema for `acl`
+
+Read-Only:
+
+- `id` (String)
+- `permissions` (Number)
+- `scheme` (String)
+
+
### Nested Schema for `stat`
diff --git a/docs/index.md b/docs/index.md
index 82720ed..4661b32 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -60,8 +60,10 @@ provider "zookeeper" {
### Optional
+- `password` (String, Sensitive) Password for digest authentication. Can be set via `ZOOKEEPER_PASSWORD` environment variable.
- `servers` (String) A comma separated list of 'host:port' pairs, pointing at ZooKeeper Server(s).
- `session_timeout` (Number) How many seconds a session is considered valid after losing connectivity. More information about ZooKeeper sessions can be found [here](#zookeeper-sessions).
+- `username` (String, Sensitive) Username for digest authentication. Can be set via `ZOOKEEPER_USERNAME` environment variable.
## Important aspects about ZooKeeper and this provider
diff --git a/docs/resources/sequential_znode.md b/docs/resources/sequential_znode.md
index e9e0fa7..e827dbc 100644
--- a/docs/resources/sequential_znode.md
+++ b/docs/resources/sequential_znode.md
@@ -62,6 +62,7 @@ resource "zookeeper_sequential_znode" "seqC" {
### Optional
+- `acl` (Block List) List of ACL entries for the ZNode. (see [below for nested schema](#nestedblock--acl))
- `data` (String) Content to store in the ZNode, as a UTF-8 string. Mutually exclusive with `data_base64`.
- `data_base64` (String) Content to store in the ZNode, as Base64 encoded bytes. Mutually exclusive with `data`.
@@ -71,6 +72,16 @@ resource "zookeeper_sequential_znode" "seqC" {
- `path` (String) Absolute path to the Sequential ZNode, once it is created. The prefix of this will match `path_prefix`.
- `stat` (List of Object) [ZooKeeper Stat Structure](https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_zkStatStructure) of the ZNode. More details about `stat` can be found [here](../../docs#the-stat-structure). (see [below for nested schema](#nestedatt--stat))
+
+### Nested Schema for `acl`
+
+Required:
+
+- `id` (String) The ID for the ACL entry. For example, user:hash in 'digest' scheme.
+- `permissions` (Number) The permissions for the ACL entry, represented as an integer bitmask.
+- `scheme` (String) The ACL scheme, such as 'world', 'digest', 'ip', 'x509'.
+
+
### Nested Schema for `stat`
diff --git a/docs/resources/znode.md b/docs/resources/znode.md
index cf136b8..dad6a13 100644
--- a/docs/resources/znode.md
+++ b/docs/resources/znode.md
@@ -35,6 +35,7 @@ resource "zookeeper_znode" "napoli_logo" {
### Optional
+- `acl` (Block List) List of ACL entries for the ZNode. (see [below for nested schema](#nestedblock--acl))
- `data` (String) Content to store in the ZNode, as a UTF-8 string. Mutually exclusive with `data_base64`.
- `data_base64` (String) Content to store in the ZNode, as Base64 encoded bytes. Mutually exclusive with `data`.
@@ -43,6 +44,16 @@ resource "zookeeper_znode" "napoli_logo" {
- `id` (String) The ID of this resource.
- `stat` (List of Object) [ZooKeeper Stat Structure](https://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#sc_zkStatStructure) of the ZNode. More details about `stat` can be found [here](../../docs#the-stat-structure). (see [below for nested schema](#nestedatt--stat))
+
+### Nested Schema for `acl`
+
+Required:
+
+- `id` (String) The ID for the ACL entry. For example, user:hash in 'digest' scheme.
+- `permissions` (Number) The permissions for the ACL entry, represented as an integer bitmask.
+- `scheme` (String) The ACL scheme, such as 'world', 'digest', 'ip', 'x509'.
+
+
### Nested Schema for `stat`
diff --git a/internal/client/client.go b/internal/client/client.go
index 88699ed..5c02566 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -30,6 +30,7 @@ type ZNode struct {
Path string
Stat *zk.Stat
Data []byte
+ ACL []zk.ACL
}
// Re-exporting errors from ZK library for better encapsulation.
@@ -64,10 +65,14 @@ const (
// DefaultZooKeeperSessionSec is the default amount of seconds configured for the
// Client timeout session, in case EnvZooKeeperSessionSec is not set.
DefaultZooKeeperSessionSec = 30
+
+ // Environment variables to provide digest auth credentials.
+ EnvZooKeeperUsername = "ZOOKEEPER_USERNAME"
+ EnvZooKeeperPassword = "ZOOKEEPER_PASSWORD"
)
// NewClient constructs a new Client instance.
-func NewClient(servers string, sessionTimeoutSec int) (*Client, error) {
+func NewClient(servers string, sessionTimeoutSec int, username string, password string) (*Client, error) {
serversSplit := strings.Split(servers, serversStringSeparator)
conn, _, err := zk.Connect(zk.FormatServers(serversSplit), time.Duration(sessionTimeoutSec)*time.Second)
@@ -75,6 +80,19 @@ func NewClient(servers string, sessionTimeoutSec int) (*Client, error) {
return nil, fmt.Errorf("unable to connect to ZooKeeper: %w", err)
}
+ if (username == "") != (password == "") {
+ return nil, fmt.Errorf("both username and password must be specified together")
+ }
+
+ if username != "" {
+ auth := "digest"
+ credentials := fmt.Sprintf("%s:%s", username, password)
+ err = conn.AddAuth(auth, []byte(credentials))
+ if err != nil {
+ return nil, fmt.Errorf("unable to add digest auth: %w", err)
+ }
+ }
+
return &Client{
zkConn: conn,
}, nil
@@ -99,16 +117,16 @@ func NewClientFromEnv() (*Client, error) {
return nil, fmt.Errorf("failed to convert '%s' to integer: %w", zkSession, err)
}
- return NewClient(zkServers, zkSessionInt)
+ zkUsername, _ := os.LookupEnv(EnvZooKeeperUsername)
+ zkPassword, _ := os.LookupEnv(EnvZooKeeperPassword)
+
+ return NewClient(zkServers, zkSessionInt, zkUsername, zkPassword)
}
// Create a ZNode at the given path.
//
// Note that any necessary ZNode parents will be created if absent.
-func (c *Client) Create(path string, data []byte) (*ZNode, error) {
- // TODO Make ACL configurable
- acl := zk.WorldACL(zk.PermRead | zk.PermWrite | zk.PermCreate | zk.PermDelete)
-
+func (c *Client) Create(path string, data []byte, acl []zk.ACL) (*ZNode, error) {
if path[len(path)-1] == zNodePathSeparator {
return nil, fmt.Errorf("non-sequential ZNode cannot have path '%s' because it ends in '%c'", path, zNodePathSeparator)
}
@@ -130,10 +148,7 @@ func (c *Client) Create(path string, data []byte) (*ZNode, error) {
// - created znode path -> `/this/is/a/path/0000000001`
//
// Note also that any necessary ZNode parents will be created if absent.
-func (c *Client) CreateSequential(path string, data []byte) (*ZNode, error) {
- // TODO Make ACL configurable
- acl := zk.WorldACL(zk.PermRead | zk.PermWrite | zk.PermCreate | zk.PermDelete)
-
+func (c *Client) CreateSequential(path string, data []byte, acl []zk.ACL) (*ZNode, error) {
return c.doCreate(path, data, zk.FlagSequence, acl)
}
@@ -202,17 +217,23 @@ func (c *Client) Read(path string) (*ZNode, error) {
return nil, fmt.Errorf("failed to read ZNode '%s': %w", path, err)
}
+ acls, _, err := c.zkConn.GetACL(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch ACLs for ZNode '%s': %w", path, err)
+ }
+
return &ZNode{
Path: path,
Stat: stat,
Data: data,
+ ACL: acls,
}, nil
}
// Update the ZNode at the given path, under the assumption that it is there.
//
// Will return an error if it doesn't already exist.
-func (c *Client) Update(path string, data []byte) (*ZNode, error) {
+func (c *Client) Update(path string, data []byte, acl []zk.ACL) (*ZNode, error) {
exists, err := c.Exists(path)
if err != nil {
return nil, err
@@ -222,6 +243,11 @@ func (c *Client) Update(path string, data []byte) (*ZNode, error) {
return nil, fmt.Errorf("failed to update ZNode '%s': does not exist", path)
}
+ _, err = c.zkConn.SetACL(path, acl, matchAnyVersion)
+ if err != nil {
+ return nil, fmt.Errorf("failed to update ZNode '%s' ACL: %w", path, err)
+ }
+
_, err = c.zkConn.Set(path, data, matchAnyVersion)
if err != nil {
return nil, fmt.Errorf("failed to update ZNode '%s': %w", path, err)
@@ -230,6 +256,11 @@ func (c *Client) Update(path string, data []byte) (*ZNode, error) {
return c.Read(path)
}
+// Close the connection.
+func (c *Client) Close() {
+ c.zkConn.Close()
+}
+
// Delete the given ZNode.
//
// Note that will also delete any child ZNode, recursively.
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index 49031ec..6d39d0a 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -3,6 +3,7 @@ package client_test
import (
"testing"
+ "github.com/go-zookeeper/zk"
testifyAssert "github.com/stretchr/testify/assert"
"github.com/tfzk/terraform-provider-zookeeper/internal/client"
)
@@ -25,7 +26,7 @@ func TestClassicCRUD(t *testing.T) {
assert.False(znodeExists)
// create
- znode, err := client.Create("/test/ClassicCRUD", []byte("one"))
+ znode, err := client.Create("/test/ClassicCRUD", []byte("one"), zk.WorldACL(zk.PermAll))
assert.NoError(err)
assert.Equal("/test/ClassicCRUD", znode.Path)
assert.Equal([]byte("one"), znode.Data)
@@ -42,7 +43,7 @@ func TestClassicCRUD(t *testing.T) {
assert.Equal([]byte("one"), znode.Data)
// update
- znode, err = client.Update("/test/ClassicCRUD", []byte("two"))
+ znode, err = client.Update("/test/ClassicCRUD", []byte("two"), zk.WorldACL(zk.PermAll))
assert.NoError(err)
assert.Equal("/test/ClassicCRUD", znode.Path)
assert.Equal([]byte("two"), znode.Data)
@@ -69,11 +70,11 @@ func TestClassicCRUD(t *testing.T) {
func TestCreateSequential(t *testing.T) {
client, assert := initTest(t)
- noPrefixSeqZNode, err := client.CreateSequential("/test/CreateSequential/", []byte("seq"))
+ noPrefixSeqZNode, err := client.CreateSequential("/test/CreateSequential/", []byte("seq"), zk.WorldACL(zk.PermAll))
assert.NoError(err)
assert.Equal("/test/CreateSequential/0000000000", noPrefixSeqZNode.Path)
- prefixSeqZNode, err := client.CreateSequential("/test/CreateSequentialWithPrefix/prefix-", []byte("seq"))
+ prefixSeqZNode, err := client.CreateSequential("/test/CreateSequentialWithPrefix/prefix-", []byte("seq"), zk.WorldACL(zk.PermAll))
assert.NoError(err)
assert.Equal("/test/CreateSequentialWithPrefix/prefix-0000000000", prefixSeqZNode.Path)
@@ -82,10 +83,75 @@ func TestCreateSequential(t *testing.T) {
assert.NoError(err)
}
+func TestDigestAuthenticationSuccess(t *testing.T) {
+ t.Setenv(client.EnvZooKeeperUsername, "username")
+ t.Setenv(client.EnvZooKeeperPassword, "password")
+ client, assert := initTest(t)
+
+ // Create a ZNode accessible only by the given user
+ acl := zk.DigestACL(zk.PermAll, "username", "password")
+ znode, err := client.Create("/auth-test/DigestAuthentication", []byte("data"), acl)
+ assert.NoError(err)
+ assert.Equal("/auth-test/DigestAuthentication", znode.Path)
+ assert.Equal([]byte("data"), znode.Data)
+ assert.Equal(acl, znode.ACL)
+
+ // Make sure it's accessible
+ znode, err = client.Read("/auth-test/DigestAuthentication")
+ assert.NoError(err)
+ assert.Equal("/auth-test/DigestAuthentication", znode.Path)
+ assert.Equal([]byte("data"), znode.Data)
+ assert.Equal(acl, znode.ACL)
+
+ // Cleanup
+ err = client.Delete("/auth-test/DigestAuthentication")
+ assert.NoError(err)
+ err = client.Delete("/auth-test")
+ assert.NoError(err)
+}
+
+func TestFailureWhenReadingZNodeWithIncorrectAuth(t *testing.T) {
+ // Create client authenticated as foo user
+ t.Setenv(client.EnvZooKeeperUsername, "foo")
+ t.Setenv(client.EnvZooKeeperPassword, "password")
+ fooClient, assert := initTest(t)
+
+ // Create a ZNode accessible only by foo user
+ acl := zk.DigestACL(zk.PermAll, "foo", "password")
+ znode, err := fooClient.Create("/auth-fail-test/AccessibleOnlyByFoo", []byte("data"), acl)
+ assert.NoError(err)
+ assert.Equal("/auth-fail-test/AccessibleOnlyByFoo", znode.Path)
+ assert.Equal([]byte("data"), znode.Data)
+ assert.Equal(acl, znode.ACL)
+
+ // Make sure it's accessible by foo user
+ znode, err = fooClient.Read("/auth-fail-test/AccessibleOnlyByFoo")
+ assert.NoError(err)
+ assert.Equal("/auth-fail-test/AccessibleOnlyByFoo", znode.Path)
+ assert.Equal([]byte("data"), znode.Data)
+ assert.Equal(acl, znode.ACL)
+
+ // Create client authenticated as bar user
+ t.Setenv(client.EnvZooKeeperUsername, "bar")
+ t.Setenv(client.EnvZooKeeperPassword, "password")
+ barClient, err := client.NewClientFromEnv()
+ assert.NoError(err)
+
+ // The node should be inaccessible by bar user
+ _, err = barClient.Read("/auth-fail-test/AccessibleOnlyByFoo")
+ assert.EqualError(err, "failed to read ZNode '/auth-fail-test/AccessibleOnlyByFoo': zk: not authenticated")
+
+ // Cleanup
+ err = fooClient.Delete("/auth-fail-test/AccessibleOnlyByFoo")
+ assert.NoError(err)
+ err = fooClient.Delete("/auth-fail-test")
+ assert.NoError(err)
+}
+
func TestFailureWhenCreatingForNonSequentialZNodeEndingInSlash(t *testing.T) {
client, assert := initTest(t)
- _, err := client.Create("/test/willFail/", nil)
+ _, err := client.Create("/test/willFail/", nil, zk.WorldACL(zk.PermAll))
assert.Error(err)
assert.Equal("non-sequential ZNode cannot have path '/test/willFail/' because it ends in '/'", err.Error())
}
@@ -93,11 +159,11 @@ func TestFailureWhenCreatingForNonSequentialZNodeEndingInSlash(t *testing.T) {
func TestFailureWhenCreatingWhenZNodeAlreadyExists(t *testing.T) {
client, assert := initTest(t)
- _, err := client.Create("/test/node", nil)
+ _, err := client.Create("/test/node", nil, zk.WorldACL(zk.PermAll))
assert.NoError(err)
- _, err = client.Create("/test/node", nil)
+ _, err = client.Create("/test/node", nil, zk.WorldACL(zk.PermAll))
assert.Error(err)
- assert.Equal("failed to create ZNode '/test/node' (size: 0, createFlags: 0, acl: [{15 world anyone}]): zk: node already exists", err.Error())
+ assert.Equal("failed to create ZNode '/test/node' (size: 0, createFlags: 0, acl: [{31 world anyone}]): zk: node already exists", err.Error())
err = client.Delete("/test")
assert.NoError(err)
@@ -110,7 +176,7 @@ func TestFailureWithNonExistingZNodes(t *testing.T) {
assert.Error(err)
assert.Equal("failed to read ZNode '/does-not-exist': zk: node does not exist", err.Error())
- _, err = client.Update("/also-does-not-exist", nil)
+ _, err = client.Update("/also-does-not-exist", nil, zk.WorldACL(zk.PermAll))
assert.Error(err)
assert.Equal("failed to update ZNode '/also-does-not-exist': does not exist", err.Error())
}
diff --git a/internal/provider/common.go b/internal/provider/common.go
index 4c2b186..9179256 100644
--- a/internal/provider/common.go
+++ b/internal/provider/common.go
@@ -3,7 +3,9 @@ package provider
import (
"encoding/base64"
"fmt"
+ "math"
+ "github.com/go-zookeeper/zk"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/tfzk/terraform-provider-zookeeper/internal/client"
@@ -31,6 +33,21 @@ func setAttributesFromZNode(rscData *schema.ResourceData, znode *client.ZNode, d
diags = append(diags, diag.FromErr(err)...)
}
+ // Convert ACLs from []zk.ACL to []map[string]interface{}
+ aclConfigs := make([]map[string]interface{}, 0, len(znode.ACL))
+ for _, acl := range znode.ACL {
+ aclConfig := map[string]interface{}{
+ "scheme": acl.Scheme,
+ "id": acl.ID,
+ "permissions": acl.Perms,
+ }
+ aclConfigs = append(aclConfigs, aclConfig)
+ }
+
+ if err := rscData.Set("acl", aclConfigs); err != nil {
+ diags = append(diags, diag.FromErr(err)...)
+ }
+
return diags
}
@@ -141,3 +158,34 @@ func getDataBytesFromResourceData(rscData *schema.ResourceData) ([]byte, error)
return nil, nil
}
+
+func parseACLsFromResourceData(rscData *schema.ResourceData) ([]zk.ACL, error) {
+ aclConfigs := rscData.Get("acl").([]interface{})
+ acls := make([]zk.ACL, 0, len(aclConfigs))
+
+ for _, aclConfig := range aclConfigs {
+ aclMap := aclConfig.(map[string]interface{})
+ scheme := aclMap["scheme"].(string)
+ id := aclMap["id"].(string)
+ permissionsValue, ok := aclMap["permissions"].(int)
+ if !ok {
+ return nil, fmt.Errorf("acl permissions value is not an integer")
+ }
+ if permissionsValue < math.MinInt32 || permissionsValue > math.MaxInt32 {
+ return nil, fmt.Errorf("acl permissions value %d is out of int32 range", permissionsValue)
+ }
+ permissions := int32(permissionsValue)
+
+ acls = append(acls, zk.ACL{
+ Scheme: scheme,
+ ID: id,
+ Perms: permissions,
+ })
+ }
+
+ if len(acls) == 0 {
+ acls = zk.WorldACL(zk.PermAll)
+ }
+
+ return acls, nil
+}
diff --git a/internal/provider/data_source_znode.go b/internal/provider/data_source_znode.go
index e371914..a4aef8c 100644
--- a/internal/provider/data_source_znode.go
+++ b/internal/provider/data_source_znode.go
@@ -29,6 +29,33 @@ func datasourceZNode() *schema.Resource {
"Use this if content is binary (i.e. sequence of bytes).",
},
"stat": statSchema(),
+ "acl": {
+ Type: schema.TypeList,
+ Computed: true,
+ Description: "List of ACL entries for the ZNode.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "scheme": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The ACL scheme, such as 'world', 'digest', " +
+ "'ip', 'x509'.",
+ },
+ "id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The ID for the ACL entry. For example, " +
+ "user:hash in 'digest' scheme.",
+ },
+ "permissions": {
+ Type: schema.TypeInt,
+ Computed: true,
+ Description: "The permissions for the ACL entry, " +
+ "represented as an integer bitmask.",
+ },
+ },
+ },
+ },
},
Description: "Provides access to the content of a " +
zNodeLinkForDesc + ". " +
diff --git a/internal/provider/data_source_znode_test.go b/internal/provider/data_source_znode_test.go
index d073189..beb783d 100644
--- a/internal/provider/data_source_znode_test.go
+++ b/internal/provider/data_source_znode_test.go
@@ -23,7 +23,7 @@ func TestAccDataSourceZNode(t *testing.T) {
data = "Forza Napoli!"
}
data "zookeeper_znode" "dst" {
- path = zookeeper_znode.src.path
+ path = zookeeper_znode.src.path
}`, srcPath,
),
Check: resource.ComposeAggregateTestCheckFunc(
@@ -58,6 +58,13 @@ func TestAccDataSourceZNode(t *testing.T) {
resource.TestCheckResourceAttrPair("data.zookeeper_znode.dst", "stat.0.num_children", "zookeeper_znode.src", "stat.0.num_children"),
resource.TestCheckResourceAttr("data.zookeeper_znode.dst", "stat.0.num_children", "0"),
+
+ resource.TestCheckResourceAttrPair("data.zookeeper_znode.dst", "acl.0.scheme", "zookeeper_znode.src", "acl.0.scheme"),
+ resource.TestCheckResourceAttr("data.zookeeper_znode.dst", "acl.0.scheme", "world"),
+ resource.TestCheckResourceAttrPair("data.zookeeper_znode.dst", "acl.0.id", "zookeeper_znode.src", "acl.0.id"),
+ resource.TestCheckResourceAttr("data.zookeeper_znode.dst", "acl.0.id", "anyone"),
+ resource.TestCheckResourceAttrPair("data.zookeeper_znode.dst", "acl.0.permissions", "zookeeper_znode.src", "acl.0.permissions"),
+ resource.TestCheckResourceAttr("data.zookeeper_znode.dst", "acl.0.permissions", "31"),
),
},
},
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index d58858d..3de7552 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -26,6 +26,20 @@ func New() (*schema.Provider, error) {
Description: "How many seconds a session is considered valid after losing connectivity. " +
"More information about ZooKeeper sessions can be found [here](#zookeeper-sessions).",
},
+ "username": {
+ Type: schema.TypeString,
+ Optional: true,
+ Sensitive: true,
+ DefaultFunc: schema.EnvDefaultFunc(client.EnvZooKeeperUsername, nil),
+ Description: "Username for digest authentication. Can be set via `ZOOKEEPER_USERNAME` environment variable.",
+ },
+ "password": {
+ Type: schema.TypeString,
+ Optional: true,
+ Sensitive: true,
+ DefaultFunc: schema.EnvDefaultFunc(client.EnvZooKeeperPassword, nil),
+ Description: "Password for digest authentication. Can be set via `ZOOKEEPER_PASSWORD` environment variable.",
+ },
},
ResourcesMap: map[string]*schema.Resource{
"zookeeper_znode": resourceZNode(),
@@ -41,9 +55,11 @@ func New() (*schema.Provider, error) {
func configureProviderContext(_ context.Context, rscData *schema.ResourceData) (interface{}, diag.Diagnostics) {
servers := rscData.Get("servers").(string)
sessionTimeout := rscData.Get("session_timeout").(int)
+ username := rscData.Get("username").(string)
+ password := rscData.Get("password").(string)
if servers != "" {
- c, err := client.NewClient(servers, sessionTimeout)
+ c, err := client.NewClient(servers, sessionTimeout, username, password)
if err != nil {
// Report inability to connect internal Client
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
index d81b33d..8363d9b 100644
--- a/internal/provider/provider_test.go
+++ b/internal/provider/provider_test.go
@@ -62,5 +62,6 @@ func confirmAllZNodeDestroyed(s *terraform.State) error {
}
}
+ zkClient.Close()
return nil
}
diff --git a/internal/provider/resource_sequential_znode.go b/internal/provider/resource_sequential_znode.go
index 9298512..70578ad 100644
--- a/internal/provider/resource_sequential_znode.go
+++ b/internal/provider/resource_sequential_znode.go
@@ -53,6 +53,34 @@ func resourceSeqZNode() *schema.Resource {
"The prefix of this will match `path_prefix`.",
},
"stat": statSchema(),
+ "acl": {
+ Type: schema.TypeList,
+ Optional: true,
+ Computed: true,
+ Description: "List of ACL entries for the ZNode.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "scheme": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The ACL scheme, such as 'world', 'digest', " +
+ "'ip', 'x509'.",
+ },
+ "id": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The ID for the ACL entry. For example, " +
+ "user:hash in 'digest' scheme.",
+ },
+ "permissions": {
+ Type: schema.TypeInt,
+ Required: true,
+ Description: "The permissions for the ACL entry, " +
+ "represented as an integer bitmask.",
+ },
+ },
+ },
+ },
},
Description: "Manages the lifecycle of a " +
zNodeLinkForDesc + ". " +
@@ -72,7 +100,12 @@ func resourceSeqZNodeCreate(_ context.Context, rscData *schema.ResourceData, prv
return diag.FromErr(err)
}
- znode, err := zkClient.CreateSequential(znodePathPrefix, dataBytes)
+ acls, err := parseACLsFromResourceData(rscData)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ znode, err := zkClient.CreateSequential(znodePathPrefix, dataBytes, acls)
if err != nil {
return diag.Errorf("Failed to create Sequential ZNode '%s': %v", znodePathPrefix, err)
}
diff --git a/internal/provider/resource_sequential_znode_test.go b/internal/provider/resource_sequential_znode_test.go
index f73d561..d0451d3 100644
--- a/internal/provider/resource_sequential_znode_test.go
+++ b/internal/provider/resource_sequential_znode_test.go
@@ -70,3 +70,78 @@ func TestAccResourceSeqZNode_FromPrefix(t *testing.T) {
},
})
}
+
+func TestAccResourceSeqZNode_DefaultACL(t *testing.T) {
+ seqFromDir := "/" + acctest.RandString(10) + "/"
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { checkPreconditions(t) },
+ ProviderFactories: providerFactoriesMap(),
+ CheckDestroy: confirmAllZNodeDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "zookeeper_sequential_znode" "default_acl" {
+ path_prefix = "%s"
+ data = "sequential znode created with default acl"
+ }`, seqFromDir,
+ ),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestMatchResourceAttr("zookeeper_sequential_znode.default_acl", "path", regexp.MustCompile(`^`+seqFromDir+`\d{10}`)),
+ resource.TestCheckResourceAttrPair("zookeeper_sequential_znode.default_acl", "path", "zookeeper_sequential_znode.default_acl", "id"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.default_acl", "data", "sequential znode created with default acl"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.default_acl", "data_base64", "c2VxdWVudGlhbCB6bm9kZSBjcmVhdGVkIHdpdGggZGVmYXVsdCBhY2w="),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.default_acl", "acl.#", "1"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.default_acl", "acl.0.scheme", "world"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.default_acl", "acl.0.id", "anyone"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.default_acl", "acl.0.permissions", "31"),
+ ),
+ },
+ {
+ ResourceName: "zookeeper_sequential_znode.default_acl",
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ },
+ })
+}
+
+func TestAccResourceSeqZNode_WithACL(t *testing.T) {
+ seqFromDir := "/" + acctest.RandString(10) + "/"
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { checkPreconditions(t) },
+ ProviderFactories: providerFactoriesMap(),
+ CheckDestroy: confirmAllZNodeDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "zookeeper_sequential_znode" "with_acl" {
+ path_prefix = "%s"
+ data = "sequential znode created with acl"
+ acl {
+ scheme = "world"
+ id = "anyone"
+ permissions = 31
+ }
+ }`, seqFromDir,
+ ),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestMatchResourceAttr("zookeeper_sequential_znode.with_acl", "path", regexp.MustCompile(`^`+seqFromDir+`\d{10}`)),
+ resource.TestCheckResourceAttrPair("zookeeper_sequential_znode.with_acl", "path", "zookeeper_sequential_znode.with_acl", "id"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.with_acl", "data", "sequential znode created with acl"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.with_acl", "data_base64", "c2VxdWVudGlhbCB6bm9kZSBjcmVhdGVkIHdpdGggYWNs"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.with_acl", "acl.#", "1"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.with_acl", "acl.0.scheme", "world"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.with_acl", "acl.0.id", "anyone"),
+ resource.TestCheckResourceAttr("zookeeper_sequential_znode.with_acl", "acl.0.permissions", "31"),
+ ),
+ },
+ {
+ ResourceName: "zookeeper_sequential_znode.with_acl",
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ },
+ })
+}
diff --git a/internal/provider/resource_znode.go b/internal/provider/resource_znode.go
index 76af5b6..4204dd9 100644
--- a/internal/provider/resource_znode.go
+++ b/internal/provider/resource_znode.go
@@ -42,6 +42,34 @@ func resourceZNode() *schema.Resource {
"Mutually exclusive with `data`.",
},
"stat": statSchema(),
+ "acl": {
+ Type: schema.TypeList,
+ Optional: true,
+ Computed: true,
+ Description: "List of ACL entries for the ZNode.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "scheme": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The ACL scheme, such as 'world', 'digest', " +
+ "'ip', 'x509'.",
+ },
+ "id": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The ID for the ACL entry. For example, " +
+ "user:hash in 'digest' scheme.",
+ },
+ "permissions": {
+ Type: schema.TypeInt,
+ Required: true,
+ Description: "The permissions for the ACL entry, " +
+ "represented as an integer bitmask.",
+ },
+ },
+ },
+ },
},
Description: "Manages the lifecycle of a " +
zNodeLinkForDesc + ". " +
@@ -61,7 +89,12 @@ func resourceZNodeCreate(_ context.Context, rscData *schema.ResourceData, prvCli
return diag.FromErr(err)
}
- znode, err := zkClient.Create(znodePath, dataBytes)
+ acls, err := parseACLsFromResourceData(rscData)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ znode, err := zkClient.Create(znodePath, dataBytes, acls)
if err != nil {
return diag.Errorf("Failed to create ZNode '%s': %v", znodePath, err)
}
@@ -98,13 +131,18 @@ func resourceZNodeUpdate(_ context.Context, rscData *schema.ResourceData, prvCli
znodePath := rscData.Id()
- if rscData.HasChanges("data", "data_base64") {
+ if rscData.HasChanges("data", "data_base64", "acl") {
dataBytes, err := getDataBytesFromResourceData(rscData)
if err != nil {
return diag.FromErr(err)
}
- znode, err := zkClient.Update(znodePath, dataBytes)
+ acls, err := parseACLsFromResourceData(rscData)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ znode, err := zkClient.Update(znodePath, dataBytes, acls)
if err != nil {
return diag.Errorf("Failed to update ZNode '%s': %v", znodePath, err)
}
diff --git a/internal/provider/resource_znode_test.go b/internal/provider/resource_znode_test.go
index f8ff177..8ad8f5e 100644
--- a/internal/provider/resource_znode_test.go
+++ b/internal/provider/resource_znode_test.go
@@ -132,3 +132,62 @@ func TestAccResourceZNode_Base64(t *testing.T) {
},
})
}
+
+func TestAccResourceZNode_DefaultACL(t *testing.T) {
+ path := "/" + acctest.RandString(10)
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { checkPreconditions(t) },
+ ProviderFactories: providerFactoriesMap(),
+ CheckDestroy: confirmAllZNodeDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "zookeeper_znode" "test_default_acl" {
+ path = "%s"
+ data = "Default ACL Test"
+ }`, path),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("zookeeper_znode.test_default_acl", "path", path),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_default_acl", "data", "Default ACL Test"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_default_acl", "acl.#", "1"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_default_acl", "acl.0.scheme", "world"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_default_acl", "acl.0.id", "anyone"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_default_acl", "acl.0.permissions", "31"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccResourceZNode_WithACL(t *testing.T) {
+ path := "/" + acctest.RandString(10)
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { checkPreconditions(t) },
+ ProviderFactories: providerFactoriesMap(),
+ CheckDestroy: confirmAllZNodeDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: fmt.Sprintf(`
+ resource "zookeeper_znode" "test_acl" {
+ path = "%s"
+ data = "ACL Test"
+ acl {
+ scheme = "world"
+ id = "anyone"
+ permissions = 31
+ }
+ }`, path),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("zookeeper_znode.test_acl", "path", path),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_acl", "data", "ACL Test"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_acl", "acl.#", "1"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_acl", "acl.0.scheme", "world"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_acl", "acl.0.id", "anyone"),
+ resource.TestCheckResourceAttr("zookeeper_znode.test_acl", "acl.0.permissions", "31"),
+ ),
+ },
+ },
+ })
+}