Skip to content

Commit

Permalink
feat(provider): Support worker filter on dynamic host catalogs
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoghx committed Oct 9, 2024
1 parent 5d6d277 commit 2a1ba22
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/resources/host_catalog_plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ resource "boundary_host_catalog_plugin" "azure_example" {
- `plugin_name` (String) The name of the plugin that should back the resource. This or plugin_id must be defined.
- `secrets_hmac` (String) The HMAC'd secrets value returned from the server.
- `secrets_json` (String, Sensitive) The secrets for the host catalog. Either values encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" to clear any existing values. NOTE: Unlike "attributes_json", removing this block will NOT clear secrets from the host catalog; this allows injecting secrets for one call, then removing them for storage.
- `worker_filter` (String) HCP Only. A filter used to control which PKI workers can handle dynamic host catalog requests.

### Read-Only

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ go 1.23.1
require (
github.com/YakDriver/regexache v0.23.0
github.com/hashicorp/boundary v0.17.1
github.com/hashicorp/boundary/api v0.0.52
github.com/hashicorp/boundary/sdk v0.0.48
github.com/hashicorp/boundary/api v0.0.53
github.com/hashicorp/boundary/sdk v0.0.49
github.com/hashicorp/cap v0.5.1-0.20240315182732-faa330bfb8df
github.com/hashicorp/cap/ldap v0.0.0-20240206183135-ed8f24513744
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0Q
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/boundary v0.17.1 h1:F7jxgYDqVJXaoc2lECKa1fNCd67+Ax7ensj0SF/k0u8=
github.com/hashicorp/boundary v0.17.1/go.mod h1:yjHRazW2F6qQMA+HZzZvnsC9QXsP6LvTtwFXX7BQSXY=
github.com/hashicorp/boundary/api v0.0.52 h1:eGs+bktswyyOQLlFHF834q/0Eh3itpvrHwsP8Uxd0Ng=
github.com/hashicorp/boundary/api v0.0.52/go.mod h1:64V8LYtPUXm6H/bhI/w0O9p8mufZlQpsbWtEQ7nP0kg=
github.com/hashicorp/boundary/sdk v0.0.48 h1:4HqyX1tS1kuaCa18OSbPGf8ZHJuwdmm1yaxr1u+nxZ4=
github.com/hashicorp/boundary/sdk v0.0.48/go.mod h1:9iOT7kDM6mYcSkKxNuZlv8rP7U5BG1kXoevjLLL8lNQ=
github.com/hashicorp/boundary/api v0.0.53 h1:4T7lJIoRf8to5j5FjawiWN4L4yolfqd4kT398Jh04So=
github.com/hashicorp/boundary/api v0.0.53/go.mod h1:cDpdeEjTB9CFMGyMJJobwN5QbbT/wUYO28qXYK9wmM8=
github.com/hashicorp/boundary/sdk v0.0.49 h1:XOb6mSKyrU/wI20+5xTYBHQUP7eIeKcLxKSCpCs4yzM=
github.com/hashicorp/boundary/sdk v0.0.49/go.mod h1:IHP79to8aIi22FiY58pgBqJL96/U9D8ZAUhS2DdC+Us=
github.com/hashicorp/cap v0.5.1-0.20240315182732-faa330bfb8df h1:GG98gdcZqXuOgY6UnPpgGfynKd492IGICpjaEz4rKmw=
github.com/hashicorp/cap v0.5.1-0.20240315182732-faa330bfb8df/go.mod h1:9XAXB89zPUrfaNzjbUG4f0/cVQ81oNx/vUudy6wnOzw=
github.com/hashicorp/cap/ldap v0.0.0-20240206183135-ed8f24513744 h1:6cU01ORaCUxRXRS0YYnmm4tMfAvFRjamhjgMrgmoWZs=
Expand Down
23 changes: 23 additions & 0 deletions internal/provider/resource_host_catalog_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ func resourceHostCatalogPlugin() *schema.Resource {
Optional: true,
Computed: true,
},
WorkerFilterKey: {
Description: "HCP Only. A filter used to control which PKI workers can handle dynamic host catalog requests.",
Type: schema.TypeString,
Optional: true,
},
internalSecretsConfigHmacKey: {
Description: "Internal only. HMAC of (serverSecretsHmac + config secrets). Used for proper secrets handling.",
Type: schema.TypeString,
Expand Down Expand Up @@ -294,6 +299,9 @@ func setFromHostCatalogPluginResponseMap(d *schema.ResourceData, raw map[string]
if err := d.Set(ScopeIdKey, raw[ScopeIdKey]); err != nil {
return err
}
if err := d.Set(WorkerFilterKey, raw[WorkerFilterKey]); err != nil {
return err
}
// Plugin stuff
{
if err := d.Set(PluginIdKey, raw[PluginIdKey]); err != nil {
Expand Down Expand Up @@ -399,6 +407,12 @@ func resourceHostCatalogPluginCreate(ctx context.Context, d *schema.ResourceData
opts = append(opts, hostcatalogs.WithDescription(descStr))
}

workerFilterVal, ok := d.GetOk(WorkerFilterKey)
if ok {
workerFilterStr := workerFilterVal.(string)
opts = append(opts, hostcatalogs.WithWorkerFilter(workerFilterStr))
}

attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
Expand Down Expand Up @@ -556,6 +570,15 @@ func resourceHostCatalogPluginUpdate(ctx context.Context, d *schema.ResourceData
}
}

if d.HasChange(WorkerFilterKey) {
opts = append(opts, hostcatalogs.DefaultWorkerFilter())
workerFilterVal, ok := d.GetOk(WorkerFilterKey)
if ok {
workerFilterStr := workerFilterVal.(string)
opts = append(opts, hostcatalogs.WithWorkerFilter(workerFilterStr))
}
}

if d.HasChange(AttributesJsonKey) {
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
Expand Down
209 changes: 209 additions & 0 deletions internal/provider/resource_host_catalog_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const (
testPluginHostCatalogDescription = "bar"
testPluginHostCatalogDescriptionUpdate = "bar foo"
testPluginHostCatalogDescriptionUpdate2 = "bar foo foo"

testPluginHostCatalogWorkerFilterUpdate = "\"dev\" in \"/tags/type\""
testPluginHostCatalogWorkerFilterUpdate2 = "\"pki\" in \"/tags/type\""
)

// expectedAttributesState is used here and in plugin host sets to control how
Expand Down Expand Up @@ -244,6 +247,212 @@ func TestAccPluginHostCatalog(t *testing.T) {
})
}

func TestAccPluginHostCatalogWithWorkerFilter(t *testing.T) {
t.Skip("Skipping test until Boundary Terraform Provider can unit tests for Boundary Enterprise only features")

tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]

resName := "boundary_host_catalog_plugin.foo"
initialValuesStr := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zap"
})
secrets_json = jsonencode({
hush = "puppies"
})
`,
testPluginHostCatalogDescription,
)

// Changed description, secrets and added worker filter.
valuesStrUpdate1 := fmt.Sprintf(`
description = "%s"
worker_filter = %q
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testPluginHostCatalogDescriptionUpdate,
testPluginHostCatalogWorkerFilterUpdate,
)

// Changed description, no secrets update, nullify worker filter.
valuesStrUpdate2 := fmt.Sprintf(`
description = "%s"
worker_filter = null
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testPluginHostCatalogDescriptionUpdate2,
)

// Same description and set a worker filter again, now empty secrets.
valuesStrUpdate3 := fmt.Sprintf(`
description = "%s"
worker_filter = %q
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
`,
testPluginHostCatalogDescriptionUpdate2,
testPluginHostCatalogWorkerFilterUpdate2,
)

// Same description, now explicitly unset secrets and blankify attrs and
// worker filter.
valuesStrUpdate4 := fmt.Sprintf(`
description = "%s"
secrets_json = "null"
`,
testPluginHostCatalogDescriptionUpdate2,
)

// Set values again
valuesStrUpdate5 := fmt.Sprintf(`
description = "%s"
worker_filter = %q
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testPluginHostCatalogDescriptionUpdate2,
testPluginHostCatalogWorkerFilterUpdate2,
)

// Explicitly set both secrets and attributes to null
valuesStrUpdate6 := fmt.Sprintf(`
description = "%s"
worker_filter = %q
attributes_json = "null"
secrets_json = "null"
`,
testPluginHostCatalogDescriptionUpdate2,
testPluginHostCatalogWorkerFilterUpdate2,
)

initialHcl := fmt.Sprintf(projPluginHostCatalogBase, initialValuesStr)
update1Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate1)
update2Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate2)
update3Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate3)
update4Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate4)
update5Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate5)
update6Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate6)

var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckHostCatalogResourceDestroy(t, provider, baseHostCatalogType),
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, firstProjectFoo, initialHcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists(provider, "boundary_scope.org1"),
testAccCheckScopeResourceExists(provider, "boundary_scope.proj1"),
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescription),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update1Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetButChanged),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate),
resource.TestCheckResourceAttr(resName, WorkerFilterKey, testPluginHostCatalogWorkerFilterUpdate),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update2Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// this runs the same HCL; mostly used in some manual checking
// to ensure update is still called even when nothing has
// changed (as we need for secrets)
Config: testConfig(url, fooOrg, firstProjectFoo, update2Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update3Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
resource.TestCheckResourceAttr(resName, WorkerFilterKey, testPluginHostCatalogWorkerFilterUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update4Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update5Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
resource.TestCheckResourceAttr(resName, WorkerFilterKey, testPluginHostCatalogWorkerFilterUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update6Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
resource.TestCheckResourceAttr(resName, WorkerFilterKey, testPluginHostCatalogWorkerFilterUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
},
})
}

func testAccCheckPluginHostCatalogResourceExists(testProvider *schema.Provider, name string, expAttrState expectedAttributesState) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
Expand Down

0 comments on commit 2a1ba22

Please sign in to comment.