From b8bdfcb4389787440f4736f4d16a012ad74962e5 Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Wed, 31 Jul 2019 09:53:20 -0700 Subject: [PATCH 1/9] Define "vault_consul_secret_backend_role" resource --- vault/provider.go | 4 + vault/resource_consul_secret_backend_role.go | 170 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 vault/resource_consul_secret_backend_role.go diff --git a/vault/provider.go b/vault/provider.go index 22148df94..13eec003b 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -276,6 +276,10 @@ var ( Resource: consulSecretBackendResource(), PathInventory: []string{"/consul/config/access"}, }, + "vault_consul_secret_backend_role": { + Resource: consulSecretBackendRoleResource(), + PathInventory: []string{"/consul/roles/{name}"}, + }, "vault_database_secret_backend_connection": { Resource: databaseSecretBackendConnectionResource(), PathInventory: []string{"/database/config/{name}"}, diff --git a/vault/resource_consul_secret_backend_role.go b/vault/resource_consul_secret_backend_role.go new file mode 100644 index 000000000..b50174bf3 --- /dev/null +++ b/vault/resource_consul_secret_backend_role.go @@ -0,0 +1,170 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/vault/api" +) + +func consulSecretBackendRoleResource() *schema.Resource { + return &schema.Resource{ + Create: consulSecretBackendRoleCreate, + Read: consulSecretBackendRoleRead, + Update: consulSecretBackendRoleUpdate, + Delete: consulSecretBackendRoleDelete, + Exists: consulSecretBackendRoleExists, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of an existing role against which to create this Consul credential", + }, + "path": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + Description: "Unique name of the Vault Consul mount to configure", + }, + "policies": { + Type: schema.TypeList, + Required: true, + Description: "List of Consul policies to associate with this role", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func consulSecretBackendRoleCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + name := d.Get("name").(string) + path := d.Get("path").(string) + policies := d.Get("policies").([]interface{}) + + reqPath := consulSecretBackendRolePath(path, name) + + payload := map[string]interface{}{ + "policies": policies, + } + + d.Partial(true) + log.Printf("[DEBUG] Configuring Consul secrets backend role at %q", reqPath) + + d.SetId(path + "," + name) + + if _, err := client.Logical().Write(reqPath, payload); err != nil { + return fmt.Errorf("Error writing role configuration for %q: %s", reqPath, err) + } + d.SetPartial("name") + d.SetPartial("path") + d.SetPartial("policies") + d.Partial(false) + + return nil +} + +func consulSecretBackendRoleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + log.Printf("[DEBUG] Reading Consul secrets backend role at %q", reqPath) + + secret, err := client.Logical().Read(reqPath) + if err != nil { + return fmt.Errorf("Error reading role configuration for %q: %s", reqPath, err) + } + + if secret == nil { + return fmt.Errorf("Resource not found") + } + + data := secret.Data + d.Set("name", name) + d.Set("path", path) + d.Set("policies", data["policies"]) + + return nil +} + +func consulSecretBackendRoleUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + d.Partial(true) + + if d.HasChange("policies") { + log.Printf("[DEBUG] Updating role configuration at %q", reqPath) + policies := d.Get("policies").([]interface{}) + + payload := map[string]interface{}{ + "policies": policies, + } + if _, err := client.Logical().Write(reqPath, payload); err != nil { + return fmt.Errorf("Error writing role configuration for %q: %s", reqPath, err) + } + log.Printf("[DEBUG] Updated role configuration at %q", reqPath) + d.SetPartial("policies") + } + + d.Partial(false) + return consulSecretBackendRoleRead(d, meta) +} + +func consulSecretBackendRoleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + log.Printf("[DEBUG] Deleting Consul backend role at %q", reqPath) + + if _, err := client.Logical().Delete(reqPath); err != nil { + return fmt.Errorf("Error deleting Consul backend role at %q: %s", reqPath, err) + } + log.Printf("[DEBUG] Deleted Consul backend role at %q", reqPath) + return nil +} + +func consulSecretBackendRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + + s := strings.Split(d.Id(), ",") + path := s[0] + name := s[1] + + reqPath := consulSecretBackendRolePath(path, name) + + log.Printf("[DEBUG] Checking Consul secrets backend role at %q", reqPath) + + secret, err := client.Logical().Read(reqPath) + if err != nil { + return false, fmt.Errorf("Error reading role configuration for %q: %s", reqPath, err) + } + + return secret != nil, nil +} + +func consulSecretBackendRolePath(path, name string) string { + return strings.Trim(path, "/") + "/roles/" + name +} From 8af6e4fcb3d9015e6fc29c70e9d6eae305d8e19d Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Tue, 10 Sep 2019 21:07:53 -0700 Subject: [PATCH 2/9] Add tests and docs --- ...esource_consul_secret_backend_role_test.go | 88 +++++++++++++++++++ .../docs/r/consul_secret_backend_role.html.md | 46 ++++++++++ 2 files changed, 134 insertions(+) create mode 100644 vault/resource_consul_secret_backend_role_test.go create mode 100644 website/docs/r/consul_secret_backend_role.html.md diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go new file mode 100644 index 000000000..4df607c7a --- /dev/null +++ b/vault/resource_consul_secret_backend_role_test.go @@ -0,0 +1,88 @@ +package vault + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/vault/api" +) + +func TestConsulSecretBackendRole(t *testing.T) { + path := acctest.RandomWithPrefix("tf-test-path") + name := acctest.RandomWithPrefix("tf-test-name") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccConsulSecretBackendRoleCheckDestroy(path, name), + Steps: []resource.TestStep{ + { + Config: testConsulSecretBackendRole_initialConfig(path, name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "path", path), + resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "name", name), + resource.TestCheckResourceAttrSet("vault_consul_secret_backend.test", "policies"), + ), + }, + { + Config: testConsulSecretBackendRole_updateConfig(path, name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "path", path), + resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "name", name), + resource.TestCheckResourceAttrSet("vault_consul_secret_backend.test", "policies"), + ), + }, + }, + }) +} + +func testAccConsulSecretBackendRoleCheckDestroy(path, name string) func(*terraform.State) error { + return func(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_consul_secret_backend_role" { + continue + } + + reqPath := consulSecretBackendRolePath(path, name) + + secret, err := client.Logical().Read(reqPath) + if err != nil { + return err + } + + if secret != nil { + return fmt.Errorf("Role %q still exists", reqPath) + } + } + } +} + +func testConsulSecretBackendRole_initialConfig(path, name string) string { + return fmt.Sprintf(` +resource "vault_consul_secret_backend_role" "test" { + path = "%s" + name = "%s" + + policies = [ + "foo" + ] +}`, path, name) +} + +func testConsulSecretBackendRole_updateConfig(path, namestring) string { + return fmt.Sprintf(` +resource "vault_consul_secret_backend_role" "test" { + path = "%s" + name = "%s" + + policies = [ + "foo", + "bar", + ] +}`, path, name) +} diff --git a/website/docs/r/consul_secret_backend_role.html.md b/website/docs/r/consul_secret_backend_role.html.md new file mode 100644 index 000000000..af1610be2 --- /dev/null +++ b/website/docs/r/consul_secret_backend_role.html.md @@ -0,0 +1,46 @@ +--- +layout: "vault" +page_title: "Vault: vault_consul_secret_backend_role resource" +sidebar_current: "docs-vault-resource-consul-secret-backend-role" +description: |- + Manages a Consul secrets role for a Consul secrets engine in Vault. +--- + +# vault\_consul\_secret\_backend\_role + +Manages a Consul secrets role for a Consul secrets engine in Vault. Consul secret backends can then issue Consul tokens. + +## Example Usage + +```hcl +resource "vault_consul_secret_backend" "test" { + path = "consul" + description = "Manages the Consul backend" + + address = "127.0.0.1:8500" + token = "4240861b-ce3d-8530-115a-521ff070dd29" +} + +resource vault_consul_secret_backend_role" "example" { + name = "test-role" + path = "${vault_consul_secret_backend.test.path}" + + policies = [ + "example-policy", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `path` - (Required) The unique of an existing Consul secrets backend mount. Must not begin or end with a `/`. + +* `name` - (Required) The name of the Consul secrets engine role to create. + +* `policies` - (Required) The list of Consul ACL policies to associate with these roles. + +## Attributes Reference + +No additional attributes are exported by this resource. From 73f06d34104fa2085010fa994412fd1b888c8fbe Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Mon, 30 Sep 2019 16:00:42 -0700 Subject: [PATCH 3/9] typo fixes --- vault/resource_consul_secret_backend_role_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go index 4df607c7a..d7852d2ab 100644 --- a/vault/resource_consul_secret_backend_role_test.go +++ b/vault/resource_consul_secret_backend_role_test.go @@ -2,7 +2,6 @@ package vault import ( "fmt" - "strings" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -59,6 +58,8 @@ func testAccConsulSecretBackendRoleCheckDestroy(path, name string) func(*terrafo return fmt.Errorf("Role %q still exists", reqPath) } } + + return nil } } @@ -74,7 +75,7 @@ resource "vault_consul_secret_backend_role" "test" { }`, path, name) } -func testConsulSecretBackendRole_updateConfig(path, namestring) string { +func testConsulSecretBackendRole_updateConfig(path, name string) string { return fmt.Sprintf(` resource "vault_consul_secret_backend_role" "test" { path = "%s" From a9264eaac3ae6b24daf74de186e4daa6f1d23f60 Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Mon, 30 Sep 2019 16:23:34 -0700 Subject: [PATCH 4/9] Add test parent resource --- ...esource_consul_secret_backend_role_test.go | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go index d7852d2ab..7cf24d697 100644 --- a/vault/resource_consul_secret_backend_role_test.go +++ b/vault/resource_consul_secret_backend_role_test.go @@ -13,13 +13,14 @@ import ( func TestConsulSecretBackendRole(t *testing.T) { path := acctest.RandomWithPrefix("tf-test-path") name := acctest.RandomWithPrefix("tf-test-name") + token := "026a0c16-87cd-4c2d-b3f3-fb539f592b7e" resource.Test(t, resource.TestCase{ Providers: testProviders, PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccConsulSecretBackendRoleCheckDestroy(path, name), Steps: []resource.TestStep{ { - Config: testConsulSecretBackendRole_initialConfig(path, name), + Config: testConsulSecretBackendRole_initialConfig(path, name, token), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "path", path), resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "name", name), @@ -27,7 +28,7 @@ func TestConsulSecretBackendRole(t *testing.T) { ), }, { - Config: testConsulSecretBackendRole_updateConfig(path, name), + Config: testConsulSecretBackendRole_updateConfig(path, name, token), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "path", path), resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "name", name), @@ -63,8 +64,17 @@ func testAccConsulSecretBackendRoleCheckDestroy(path, name string) func(*terrafo } } -func testConsulSecretBackendRole_initialConfig(path, name string) string { +func testConsulSecretBackendRole_initialConfig(path, name, token string) string { return fmt.Sprintf(` +resource "vault_consul_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = 3600 + max_lease_ttl_seconds = 86400 + address = "127.0.0.1:8500" + token = "%s" +} + resource "vault_consul_secret_backend_role" "test" { path = "%s" name = "%s" @@ -72,11 +82,20 @@ resource "vault_consul_secret_backend_role" "test" { policies = [ "foo" ] -}`, path, name) +}`, path, token, path, name) } -func testConsulSecretBackendRole_updateConfig(path, name string) string { +func testConsulSecretBackendRole_updateConfig(path, name, token string) string { return fmt.Sprintf(` +resource "vault_consul_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = 3600 + max_lease_ttl_seconds = 86400 + address = "127.0.0.1:8500" + token = "%s" +} + resource "vault_consul_secret_backend_role" "test" { path = "%s" name = "%s" @@ -85,5 +104,5 @@ resource "vault_consul_secret_backend_role" "test" { "foo", "bar", ] -}`, path, name) +}`, path, token, path, name) } From 4713844e2f5195c70579cb16cc648c5f95eb5d78 Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Mon, 30 Sep 2019 16:33:13 -0700 Subject: [PATCH 5/9] address tested resource name --- vault/resource_consul_secret_backend_role_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go index 7cf24d697..5d3ab28b8 100644 --- a/vault/resource_consul_secret_backend_role_test.go +++ b/vault/resource_consul_secret_backend_role_test.go @@ -22,17 +22,17 @@ func TestConsulSecretBackendRole(t *testing.T) { { Config: testConsulSecretBackendRole_initialConfig(path, name, token), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "path", path), - resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "name", name), - resource.TestCheckResourceAttrSet("vault_consul_secret_backend.test", "policies"), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "path", path), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "name", name), + resource.TestCheckResourceAttrSet("vault_consul_secret_backend_role.test", "policies"), ), }, { Config: testConsulSecretBackendRole_updateConfig(path, name, token), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "path", path), - resource.TestCheckResourceAttr("vault_consul_secret_backend.test", "name", name), - resource.TestCheckResourceAttrSet("vault_consul_secret_backend.test", "policies"), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "path", path), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "name", name), + resource.TestCheckResourceAttrSet("vault_consul_secret_backend_role.test", "policies"), ), }, }, From a5b88f6635835804f56f3c824119c4570b1dd94c Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Mon, 30 Sep 2019 16:52:25 -0700 Subject: [PATCH 6/9] Use parent resource as a dependency --- vault/resource_consul_secret_backend_role_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go index 5d3ab28b8..d83a7cca1 100644 --- a/vault/resource_consul_secret_backend_role_test.go +++ b/vault/resource_consul_secret_backend_role_test.go @@ -76,13 +76,13 @@ resource "vault_consul_secret_backend" "test" { } resource "vault_consul_secret_backend_role" "test" { - path = "%s" + path = "${vault_consul_secret_backend.test.path}" name = "%s" policies = [ "foo" ] -}`, path, token, path, name) +}`, path, token, name) } func testConsulSecretBackendRole_updateConfig(path, name, token string) string { @@ -97,12 +97,12 @@ resource "vault_consul_secret_backend" "test" { } resource "vault_consul_secret_backend_role" "test" { - path = "%s" + path = "${vault_consul_secret_backend.test.path}" name = "%s" policies = [ "foo", "bar", ] -}`, path, token, path, name) +}`, path, token, name) } From c18e9f1834f84168bf248c18ffe75f1247074d8b Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Mon, 30 Sep 2019 16:54:18 -0700 Subject: [PATCH 7/9] fix documentation typo --- website/docs/r/consul_secret_backend_role.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/consul_secret_backend_role.html.md b/website/docs/r/consul_secret_backend_role.html.md index af1610be2..39b96fd26 100644 --- a/website/docs/r/consul_secret_backend_role.html.md +++ b/website/docs/r/consul_secret_backend_role.html.md @@ -35,7 +35,7 @@ resource vault_consul_secret_backend_role" "example" { The following arguments are supported: -* `path` - (Required) The unique of an existing Consul secrets backend mount. Must not begin or end with a `/`. +* `path` - (Required) The unique name of an existing Consul secrets backend mount. Must not begin or end with a `/`. * `name` - (Required) The name of the Consul secrets engine role to create. From ce051b47c06677c6eab9318701409ade266d9178 Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Mon, 30 Sep 2019 17:20:55 -0700 Subject: [PATCH 8/9] fix tests to use proper helper method --- vault/resource_consul_secret_backend_role_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vault/resource_consul_secret_backend_role_test.go b/vault/resource_consul_secret_backend_role_test.go index d83a7cca1..13cb4c675 100644 --- a/vault/resource_consul_secret_backend_role_test.go +++ b/vault/resource_consul_secret_backend_role_test.go @@ -24,7 +24,7 @@ func TestConsulSecretBackendRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "path", path), resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "name", name), - resource.TestCheckResourceAttrSet("vault_consul_secret_backend_role.test", "policies"), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "policies.0", "foo"), ), }, { @@ -32,7 +32,8 @@ func TestConsulSecretBackendRole(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "path", path), resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "name", name), - resource.TestCheckResourceAttrSet("vault_consul_secret_backend_role.test", "policies"), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "policies.0", "foo"), + resource.TestCheckResourceAttr("vault_consul_secret_backend_role.test", "policies.1", "bar"), ), }, }, From 6a596aca9b8254794c0a27560078cd1451533325 Mon Sep 17 00:00:00 2001 From: Robert Paprocki Date: Wed, 9 Oct 2019 20:52:56 -0700 Subject: [PATCH 9/9] lowercase error strings --- vault/resource_consul_secret_backend_role.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vault/resource_consul_secret_backend_role.go b/vault/resource_consul_secret_backend_role.go index b50174bf3..15924a90a 100644 --- a/vault/resource_consul_secret_backend_role.go +++ b/vault/resource_consul_secret_backend_role.go @@ -61,7 +61,7 @@ func consulSecretBackendRoleCreate(d *schema.ResourceData, meta interface{}) err d.SetId(path + "," + name) if _, err := client.Logical().Write(reqPath, payload); err != nil { - return fmt.Errorf("Error writing role configuration for %q: %s", reqPath, err) + return fmt.Errorf("error writing role configuration for %q: %s", reqPath, err) } d.SetPartial("name") d.SetPartial("path") @@ -84,11 +84,11 @@ func consulSecretBackendRoleRead(d *schema.ResourceData, meta interface{}) error secret, err := client.Logical().Read(reqPath) if err != nil { - return fmt.Errorf("Error reading role configuration for %q: %s", reqPath, err) + return fmt.Errorf("error reading role configuration for %q: %s", reqPath, err) } if secret == nil { - return fmt.Errorf("Resource not found") + return fmt.Errorf("resource not found") } data := secret.Data @@ -118,7 +118,7 @@ func consulSecretBackendRoleUpdate(d *schema.ResourceData, meta interface{}) err "policies": policies, } if _, err := client.Logical().Write(reqPath, payload); err != nil { - return fmt.Errorf("Error writing role configuration for %q: %s", reqPath, err) + return fmt.Errorf("error writing role configuration for %q: %s", reqPath, err) } log.Printf("[DEBUG] Updated role configuration at %q", reqPath) d.SetPartial("policies") @@ -140,7 +140,7 @@ func consulSecretBackendRoleDelete(d *schema.ResourceData, meta interface{}) err log.Printf("[DEBUG] Deleting Consul backend role at %q", reqPath) if _, err := client.Logical().Delete(reqPath); err != nil { - return fmt.Errorf("Error deleting Consul backend role at %q: %s", reqPath, err) + return fmt.Errorf("error deleting Consul backend role at %q: %s", reqPath, err) } log.Printf("[DEBUG] Deleted Consul backend role at %q", reqPath) return nil @@ -159,7 +159,7 @@ func consulSecretBackendRoleExists(d *schema.ResourceData, meta interface{}) (bo secret, err := client.Logical().Read(reqPath) if err != nil { - return false, fmt.Errorf("Error reading role configuration for %q: %s", reqPath, err) + return false, fmt.Errorf("error reading role configuration for %q: %s", reqPath, err) } return secret != nil, nil