Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Nomad secret backend #923

Merged
merged 9 commits into from
Dec 11, 2020
14 changes: 14 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ func GetTestADCreds(t *testing.T) (string, string, string) {
return adBindDN, adBindPass, adURL
}

func GetTestNomadCreds(t *testing.T) (string, string) {
address := os.Getenv("NOMAD_ADDR")
token := os.Getenv("NOMAD_TOKEN")

if address == "" {
t.Skip("NOMAD_ADDR not set")
}
if token == "" {
t.Skip("NOMAD_TOKEN not set")
}

return address, token
}

func TestCheckResourceAttrJSON(name, key, expectedValue string) resource.TestCheckFunc {
return func(s *terraform.State) error {
resourceState, ok := s.RootModule().Resources[name]
Expand Down
71 changes: 71 additions & 0 deletions vault/data_source_nomad_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package vault

import (
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/vault/api"
)

func nomadAccessCredentialsDataSource() *schema.Resource {
return &schema.Resource{
Read: readNomadCredsResource,
Schema: map[string]*schema.Schema{
"backend": {
Type: schema.TypeString,
Required: true,
Description: "Nomad secret backend to generate tokens from.",
},
"role": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Name of the role.",
},
"accessor_id": {
Type: schema.TypeString,
Computed: true,
Description: "The public identifier for a specific token. It can be used to look up information about a token or to revoke a token.",
},
"secret_id": {
Type: schema.TypeString,
Computed: true,
Description: "Used to make requests to Nomad and should be kept private.",
},
},
}
}

func readNomadCredsResource(d *schema.ResourceData, meta interface{}) error {
client := meta.(*api.Client)
backend := d.Get("backend").(string)
role := d.Get("role").(string)
path := fmt.Sprintf("%s/creds/%s", backend, role)

secret, err := client.Logical().Read(path)
if err != nil {
return fmt.Errorf("error reading from Vault: %s", err)
}
log.Printf("[DEBUG] Read %q from Vault", path)

if secret == nil {
return fmt.Errorf("no role found at %q", path)
}

accessorID := secret.Data["accessor_id"].(string)
if accessorID == "" {
return fmt.Errorf("accessor_id is not set in response")
}

secretID := secret.Data["secret_id"].(string)
if secretID == "" {
return fmt.Errorf("secret_id is not set in response")
}

d.SetId(accessorID)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use path here? Granted, we aren't using the ID at all that I can see

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Each call to this endpoint generates a new token, so we want the ID to be the unique identifier of that specific generated token. Path would be too generic I think?

d.Set("accessor_id", accessorID)
d.Set("secret_id", secretID)

return nil
}
96 changes: 96 additions & 0 deletions vault/data_source_nomad_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package vault

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-provider-vault/util"
)

func TestAccDataSourceNomadAccessCredentialsClientBasic(t *testing.T) {
backend := acctest.RandomWithPrefix("tf-test-nomad")
address, token := util.GetTestNomadCreds(t)

resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { util.TestAccPreCheck(t) },
Steps: []resource.TestStep{
{
Config: testAccDataSourceNomadAccessCredentialsConfig(backend, address, token, "test"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "secret_id"),
resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "accessor_id"),
),
},
},
})
}

func TestAccDataSourceNomadAccessCredentialsManagementBasic(t *testing.T) {
backend := acctest.RandomWithPrefix("tf-test-nomad")
address, token := util.GetTestNomadCreds(t)

resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() { util.TestAccPreCheck(t) },
Steps: []resource.TestStep{
{
Config: testAccDataSourceNomadAccessCredentialsManagementConfig(backend, address, token, "test"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "secret_id"),
resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "accessor_id"),
),
},
},
})
}

func testAccDataSourceNomadAccessCredentialsConfig(backend, address, token, role string) string {
return fmt.Sprintf(`
resource "vault_nomad_secret_backend" "config" {
backend = "%s"
description = "test description"
default_lease_ttl_seconds = "3600"
max_lease_ttl_seconds = "7200"
address = "%s"
token = "%s"
}

resource "vault_nomad_secret_role" "test" {
backend = vault_nomad_secret_backend.config.backend
role = "%s"
policies = ["reaodnly"]
}

data "vault_nomad_access_token" "token" {
backend = vault_nomad_secret_backend.config.backend
role = vault_nomad_secret_role.test.role
}
`, backend, address, token, role)
}

func testAccDataSourceNomadAccessCredentialsManagementConfig(backend, address, token, role string) string {
return fmt.Sprintf(`
resource "vault_nomad_secret_backend" "config" {
backend = "%s"
description = "test description"
default_lease_ttl_seconds = "3600"
max_lease_ttl_seconds = "7200"
address = "%s"
token = "%s"
}

resource "vault_nomad_secret_role" "test" {
backend = vault_nomad_secret_backend.config.backend
role = "%s"
type = "management"
}

data "vault_nomad_access_token" "token" {
backend = vault_nomad_secret_backend.config.backend
role = vault_nomad_secret_role.test.role
}
`, backend, address, token, role)
}
16 changes: 16 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ var (
Resource: adAccessCredentialsDataSource(),
PathInventory: []string{"/ad/creds/{role}"},
},
"vault_nomad_access_token": {
Resource: nomadAccessCredentialsDataSource(),
PathInventory: []string{"/nomad/creds/{role}"},
},
"vault_aws_access_credentials": {
Resource: awsAccessCredentialsDataSource(),
PathInventory: []string{"/aws/creds"},
Expand Down Expand Up @@ -466,6 +470,18 @@ var (
Resource: ldapAuthBackendGroupResource(),
PathInventory: []string{"/auth/ldap/groups/{name}"},
},
"vault_nomad_secret_backend": {
Resource: nomadSecretAccessBackendResource(),
PathInventory: []string{"/nomad"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Should /nomad/config/access be part of PathInventory here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@calvn Good question! Throughout the provider the mount and the config are the same resource. We derive the full path nomad/config/access from the backend. Since we derive it, /nomad is the default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nomad/config/lease is slightly different so I didn't add it to this resource. We could but it felt different enough to have a separate resource.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@calvn Having thought about combining nomad/config/lease with the other configs, I decided to keep them as two separate resources. This is simply to reduce the amount of operations create/update/read need to do for all three paths (/nomad, /nomad/config/access and /nomad/config/lease).

Copy link
Member

Choose a reason for hiding this comment

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

Is PathInventory used for routing? Or is is just for documentation purposes?

To Calvin's question this function does seem to cover the /nomad/config/access endpoint?

Copy link
Contributor Author

@jasonodonnell jasonodonnell Dec 2, 2020

Choose a reason for hiding this comment

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

I can try in the morning having both but pointing to the same resource handler, maybe that won't cause issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update: both can't be present because the resource name is the key and you can't duplicate it.

I changed the PathInventory to be /nomad/config/access. This did not have any regressions on the resources and is probably a better self-documenting change.

Copy link
Member

Choose a reason for hiding this comment

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

PathInventory is a list, so it could also be this:

		"vault_nomad_secret_backend": {
			Resource:      nomadSecretAccessBackendResource(),
			PathInventory: []string{
				"/nomad",
				"/nomad/config/access",
			},

But I think setting it to just "/nomad/config/access" makes sense 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh derp, yeah, that did work! Good call!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

/nomad, /nomad/config/access and /nomad/config/lease are merged into a single resource vault_nomad_secret_backend.

},
"vault_nomad_secret_lease": {
jasonodonnell marked this conversation as resolved.
Show resolved Hide resolved
Resource: nomadSecretLeaseBackendResource(),
PathInventory: []string{"/nomad/config/lease"},
},
"vault_nomad_secret_role": {
Resource: nomadSecretBackendRoleResource(),
PathInventory: []string{"/nomad/role/{role}"},
},
"vault_policy": {
Resource: policyResource(),
PathInventory: []string{"/sys/policy/{name}"},
Expand Down
Loading