diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c1042956 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch a test function", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "args": [ + "-test.v", + "-test.run", + "^${selectedText}$" + ], + "showLog": true, + "envFile": "${workspaceFolder}/.vscode/private.env" + } + ] +} diff --git a/.vscode/private.env b/.vscode/private.env new file mode 100644 index 00000000..231960e0 --- /dev/null +++ b/.vscode/private.env @@ -0,0 +1,3 @@ +TF_ACC=1 +TF_LOG=INFO +GOFLAGS='-mod=readonly' diff --git a/README.md b/README.md index d9957567..ee7048d5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ ![](boundary.png) -Terraform Provider Boundary -================== +# Terraform Provider Boundary Available in the [Terraform Registry](https://registry.terraform.io/providers/hashicorp/boundary/latest). -Requirements ------------- +## Requirements - [Terraform](https://www.terraform.io/downloads.html) >= 0.12.x - [Go](https://golang.org/doc/install) >= 1.20 -Building The Provider ---------------------- +## Building The Provider 1. Clone the repository 1. Enter the repository directory @@ -22,32 +19,30 @@ You'll need to ensure that your Terraform file contains the information necessar ```hcl terraform { - required_providers { - boundary = { - source = "localhost/providers/boundary" - version = "0.0.1" - } - } + required_providers { + boundary = { + source = "localhost/providers/boundary" + version = "0.0.1" + } + } } ``` -Adding Dependencies ---------------------- +## Adding Dependencies This provider uses [Go modules](https://github.com/golang/go/wiki/Modules). Please see the Go documentation for the most up to date information about using Go modules. To add a new dependency `github.com/author/dependency` to your Terraform provider: -``` +```shell go get github.com/author/dependency go mod tidy ``` Then commit the changes to `go.mod` and `go.sum`. -Developing the Provider ---------------------------- +## Developing the Provider If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (see [Requirements](#requirements) above). @@ -72,17 +67,32 @@ $ make testacc For more details on the docker image and troubleshooting see the [boundary testing doc](https://github.com/hashicorp/boundary/blob/main/CONTRIBUTING.md#testing). -Generating Docs ----------------------- +## Debugging the provider + +If you're using vscode, this provider has delve debugging baked into the [.vscode](https://github.com/hashicorp/terraform-provider-boundary/tree/main/.vscode) directory. To debug you can find instructions [here.](https://dev.to/drewmullen/vscode-terraform-provider-development-setup-debugging-6bn#usage) + +## Generating Docs + +This provider uses the [terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs/) tool to generate documentation. Each resource and data source requires a [templates](https://github.com/hashicorp/terraform-provider-boundary/tree/main/templates) and [examples](https://github.com/hashicorp/terraform-provider-boundary/tree/main/examples) then you execute a binary generate the docs which are placed in [./docs](https://github.com/hashicorp/terraform-provider-boundary/tree/main/docs). + +### New Resources & Data Sources (documentation templates) + +If you're adding a new resource or a data source, you must include a documentation template for your new R/DS in [./templates](https://github.com/hashicorp/terraform-provider-boundary/tree/main/templates) directory. This provider uses the [terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs/) tool to generate documentation. For full details see the link. + +### Documentation Examples for Resources & Data Sources + +Examples are required for all resources and data sources. This provider uses the [terraform-plugin-docs](https://github.com/hashicorp/terraform-plugin-docs/) tool to generate documentation. To add examples, add a new file to the [./examples](https://github.com/hashicorp/terraform-provider-boundary/tree/main/examples) directory or update the existing files. Next make sure the corresponding template in [./templates](https://github.com/hashicorp/terraform-provider-boundary/tree/main/templates) directory references this example. + +### Generating From the root of the repo run: -``` +```shell go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs ``` -Using the provider ----------------------- +## Using the provider + Please see our detailed docs for individual resource usage. Below is a complex example using the Boundary provider to configure all resource types available: ```hcl @@ -140,7 +150,7 @@ resource "boundary_user" "users" { scope_id = boundary_scope.corp.id } -// organization level group for readonly users +// organization level group for readonly users resource "boundary_group" "readonly" { name = "readonly" description = "Organization group for readonly users" diff --git a/docs/data-sources/scope.md b/docs/data-sources/scope.md index 7f3966df..c367e851 100644 --- a/docs/data-sources/scope.md +++ b/docs/data-sources/scope.md @@ -1,5 +1,4 @@ --- -# generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "boundary_scope Data Source - terraform-provider-boundary" subcategory: "" description: |- @@ -9,18 +8,19 @@ description: |- # boundary_scope (Data Source) The scope data source allows you to discover an existing Boundary scope by name. +Please note that the Global scope will always have an id of "global", and does not need to be discovered with this data source. ## Example Usage ```terraform # Retrieve the ID of a Boundary project data "boundary_scope" "org" { - name = "SecOps" + name = "SecOps" parent_scope_id = "global" } data "boundary_scope" "project" { - name = "2111" + name = "2111" parent_scope_id = data.boundary_scope.id } ``` diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 00000000..4456c917 --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,64 @@ +--- +page_title: "boundary_user Data Source - terraform-provider-boundary" +subcategory: "" +description: |- + The user data source allows you to find a Boundary user. +--- + +# boundary_user (Data Source) + +The user data source allows you to find a Boundary user. + +## Example Usage + +```terraform +# Retrieve a user from the global scope + +data "boundary_user" "global_scope_admin" { + name = "admin" +} + +# User from a org scope + +data "boundary_user" "org_user" { + name = "username" + scope_id = data.boundary_scope.org.id +} + +data "boundary_scope" "org" { + name = "my-org" + parent_scope_id = data.boundary_scope.org.id +} +``` + + +## Schema + +### Required + +- `name` (String) The username to search for. + +### Optional + +- `scope_id` (String) The scope ID in which the resource is created. Defaults `global` if unset. + +### Read-Only + +- `account_ids` (Set of String) Account ID's to associate with this user resource. +- `authorized_actions` (List of String) A list of actions that the worker is entitled to perform. +- `description` (String) The user description. +- `id` (String) The ID of the user. +- `login_name` (String) Login name for user. +- `primary_account_id` (String) Primary account ID. +- `scope` (List of Object) (see [below for nested schema](#nestedatt--scope)) + + +### Nested Schema for `scope` + +Read-Only: + +- `description` (String) +- `id` (String) +- `name` (String) +- `parent_scope_id` (String) +- `type` (String) diff --git a/examples/data-sources/boundary_scope/data-source.tf b/examples/data-sources/boundary_scope/data-source.tf index 049ce21c..3eb375d3 100644 --- a/examples/data-sources/boundary_scope/data-source.tf +++ b/examples/data-sources/boundary_scope/data-source.tf @@ -1,10 +1,10 @@ # Retrieve the ID of a Boundary project data "boundary_scope" "org" { - name = "SecOps" + name = "SecOps" parent_scope_id = "global" } data "boundary_scope" "project" { - name = "2111" + name = "2111" parent_scope_id = data.boundary_scope.id -} \ No newline at end of file +} diff --git a/examples/data-sources/boundary_user/data-source.tf b/examples/data-sources/boundary_user/data-source.tf new file mode 100644 index 00000000..a5ef3c3c --- /dev/null +++ b/examples/data-sources/boundary_user/data-source.tf @@ -0,0 +1,17 @@ +# Retrieve a user from the global scope + +data "boundary_user" "global_scope_admin" { + name = "admin" +} + +# User from a org scope + +data "boundary_user" "org_user" { + name = "username" + scope_id = data.boundary_scope.org.id +} + +data "boundary_scope" "org" { + name = "my-org" + parent_scope_id = data.boundary_scope.org.id +} diff --git a/go.mod b/go.mod index 28bf745c..82b488cf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hashicorp/terraform-provider-boundary go 1.21 require ( + github.com/YakDriver/regexache v0.23.0 github.com/hashicorp/boundary v0.13.1-0.20231012004550-1ed0a13004b9 github.com/hashicorp/boundary/api v0.0.41 github.com/hashicorp/boundary/sdk v0.0.37 diff --git a/go.sum b/go.sum index d4baa6f1..3542aa18 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/YakDriver/regexache v0.23.0 h1:kv3j4XKhbx/vqUilSBgizXDUXHvvH1KdYekdmGwz4C4= +github.com/YakDriver/regexache v0.23.0/go.mod h1:K4BZ3MYKAqSFbYWqmbsG+OzYUDyJjnMEr27DJEsVG3U= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= diff --git a/internal/provider/const.go b/internal/provider/const.go index 1e069257..1730403b 100644 --- a/internal/provider/const.go +++ b/internal/provider/const.go @@ -46,4 +46,9 @@ const ( internalForceUpdateKey = "internal_force_update" // workerFilter is used for common "worker_filter" resource attribute WorkerFilterKey = "worker_filter" + // common User arguments + LoginNameKey = "login_name" + PrimaryAccountID = "primary_account_id" + ScopeKey = "scope" + ParentScopeId = "parent_scope_id" ) diff --git a/internal/provider/data_source_user.go b/internal/provider/data_source_user.go new file mode 100644 index 00000000..29f6bc32 --- /dev/null +++ b/internal/provider/data_source_user.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/api/users" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceUser() *schema.Resource { + return &schema.Resource{ + Description: "The user data source allows you to find a Boundary user.", + ReadContext: dataSourceUserRead, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of the user.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "The username to search for.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + DescriptionKey: { + Description: "The user description.", + Type: schema.TypeString, + Computed: true, + }, + ScopeIdKey: { + Description: "The scope ID in which the resource is created. Defaults `global` if unset.", + Type: schema.TypeString, + Optional: true, + Default: "global", + ValidateFunc: validation.StringIsNotEmpty, + }, + userAccountIDsKey: { + Description: "Account ID's to associate with this user resource.", + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + ScopeKey: { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + IDKey: { + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Type: schema.TypeString, + Computed: true, + }, + TypeKey: { + Type: schema.TypeString, + Computed: true, + }, + DescriptionKey: { + Type: schema.TypeString, + Computed: true, + }, + ParentScopeId: { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + authorizedActions: { + Description: "A list of actions that the worker is entitled to perform.", + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + LoginNameKey: { + Description: "Login name for user.", + Type: schema.TypeString, + Computed: true, + }, + PrimaryAccountID: { + Description: "Primary account ID.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + usrs := users.NewClient(md.client) + + opts := []users.Option{} + + // Get user ID using name + name := d.Get(NameKey).(string) + scopeID := d.Get(ScopeIdKey).(string) + + opts = append(opts, users.WithFilter(FilterWithItemNameMatches(name))) + + usersList, err := usrs.List(ctx, scopeID, opts...) + if err != nil { + return diag.Errorf("error calling list user: %v", err) + } + users := usersList.GetItems() + + // check length, 0 means no user, > 1 means too many + if len(users) == 0 { + return diag.Errorf("no matching user found: %v", err) + } + + if len(users) > 1 { + return diag.Errorf("error found more than 1 user: %v", err) + } + + if err := setFromUserItem(d, *users[0]); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func setFromUserItem(d *schema.ResourceData, user users.User) error { + if err := d.Set(NameKey, user.Name); err != nil { + return err + } + if err := d.Set(DescriptionKey, user.Description); err != nil { + return err + } + if err := d.Set(ScopeIdKey, user.ScopeId); err != nil { + return err + } + if err := d.Set(userAccountIDsKey, user.AccountIds); err != nil { + return err + } + if err := d.Set(authorizedActions, user.AuthorizedActions); err != nil { + return err + } + if err := d.Set(LoginNameKey, user.LoginName); err != nil { + return err + } + if err := d.Set(PrimaryAccountID, user.PrimaryAccountId); err != nil { + return err + } + + d.Set(ScopeKey, flattenScopeInfo(user.Scope)) + + d.SetId(user.Id) + return nil +} + +func flattenScopeInfo(scope *scopes.ScopeInfo) []interface{} { + if scope == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if v := scope.Id; v != "" { + m[IDKey] = v + } + if v := scope.Type; v != "" { + m[TypeKey] = v + } + if v := scope.Description; v != "" { + m[DescriptionKey] = v + } + if v := scope.ParentScopeId; v != "" { + m[ParentScopeId] = v + } + if v := scope.Name; v != "" { + m[NameKey] = v + } + + return []interface{}{m} +} diff --git a/internal/provider/data_source_user_test.go b/internal/provider/data_source_user_test.go new file mode 100644 index 00000000..8ad1c8b1 --- /dev/null +++ b/internal/provider/data_source_user_test.go @@ -0,0 +1,95 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var ( + orgUserDataSource = fmt.Sprintf(` +resource "boundary_user" "org1" { + name = "test" + description = "%s" + scope_id = boundary_scope.org1.id + depends_on = [boundary_role.org1_admin] +} +data "boundary_user" "org1" { + name = "test" + scope_id = boundary_scope.org1.id + depends_on = [boundary_user.org1] +}`, fooDescription) + + globalUserDataSource = ` +data "boundary_user" "admin" { + name = "admin" + depends_on = [boundary_role.org1_admin] +}` +) + +// NOTE: this test also tests out the direct token auth mechanism. + +func TestAccUserDataSource_basicOrgUser(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + token := tc.Token().Token + + resourceName := "boundary_user.org1" + dataSourceName := "data.boundary_user.org1" + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckUserResourceDestroy(t, provider), + Steps: []resource.TestStep{ + { + // test create + Config: testConfigWithToken(url, token, fooOrg, orgUserDataSource), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserResourceExists(provider, resourceName), + resource.TestCheckResourceAttr(dataSourceName, DescriptionKey, fooDescription), + resource.TestCheckResourceAttr(dataSourceName, NameKey, "test"), + ), + }, + }, + }) +} + +func TestAccUserDataSource_globalAdminUser(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + token := tc.Token().Token + + dataSourceName := "data.boundary_user.admin" + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + Steps: []resource.TestStep{ + { + Config: testConfigWithToken(url, token, fooOrg, globalUserDataSource), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, NameKey, "admin"), + resource.TestCheckResourceAttr(dataSourceName, DescriptionKey, "Initial admin user within the \"global\" scope"), + resource.TestCheckResourceAttr(dataSourceName, LoginNameKey, "testuser"), + resource.TestMatchResourceAttr(dataSourceName, IDKey, regexache.MustCompile(`^u_.+`)), + resource.TestMatchResourceAttr(dataSourceName, PrimaryAccountID, regexache.MustCompile(`^acctpw_.+`)), + resource.TestCheckResourceAttr(dataSourceName, "authorized_actions.#", "7"), + resource.TestCheckResourceAttr(dataSourceName, "scope.0.name", "global"), + resource.TestCheckResourceAttr(dataSourceName, "scope.0.id", "global"), + resource.TestCheckResourceAttr(dataSourceName, "scope.0.type", "global"), + resource.TestCheckResourceAttr(dataSourceName, "scope.0.description", "Global Scope"), + ), + }, + }, + }) +} diff --git a/internal/provider/filter.go b/internal/provider/filter.go new file mode 100644 index 00000000..382b6196 --- /dev/null +++ b/internal/provider/filter.go @@ -0,0 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import "fmt" + +func FilterWithItemNameMatches(name string) string { + return fmt.Sprintf("\"/item/name\" matches \"%s\"", name) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 24f8c45b..0ab7fca7 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -129,6 +129,7 @@ func New() *schema.Provider { }, DataSourcesMap: map[string]*schema.Resource{ "boundary_scope": dataSourceScope(), + "boundary_user": dataSourceUser(), }, } diff --git a/templates/data-sources/scope.md.tmpl b/templates/data-sources/scope.md.tmpl new file mode 100644 index 00000000..8c5d282a --- /dev/null +++ b/templates/data-sources/scope.md.tmpl @@ -0,0 +1,17 @@ +--- +page_title: "boundary_scope Data Source - terraform-provider-boundary" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# boundary_scope (Data Source) + +{{ .Description | trimspace }} +Please note that the Global scope will always have an id of "global", and does not need to be discovered with this data source. + +## Example Usage + +{{tffile "examples/data-sources/boundary_scope/data-source.tf"}} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/data-sources/user.md.tmpl b/templates/data-sources/user.md.tmpl new file mode 100644 index 00000000..f37de4d1 --- /dev/null +++ b/templates/data-sources/user.md.tmpl @@ -0,0 +1,16 @@ +--- +page_title: "boundary_user Data Source - terraform-provider-boundary" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# boundary_user (Data Source) + +{{ .Description | trimspace }} + +## Example Usage + +{{tffile "examples/data-sources/boundary_user/data-source.tf"}} + +{{ .SchemaMarkdown | trimspace }}