From cfc57d5e254cad10c4a67bd4e4f2277c0f718f2f Mon Sep 17 00:00:00 2001 From: grantorchard Date: Fri, 25 Nov 2022 03:28:27 +1100 Subject: [PATCH] Add support for boundary_self_managed_worker resource. (#293) * add initial prototype of self managed worker resource and associated tests * Add self managed worker resource and associated tests * Update internal/provider/resource_self_managed_worker.go Accept suggested schema changes for tags Co-authored-by: Johan Brandhorst-Satzkorn * add environment variable check for worker led auth testing * change resource name from self_managed_worker to worker for consistency with the api * update tag schemas, ensure all attributes have descriptions * add generated docs for worker * add examples for worker resource * add examples for worker resource * remove merge conflict * Remove funtionality for reflecting tags in the schema * remove unused attributes (all tags) from schema, updates tests to use 'boundary_worker' on resource checks Co-authored-by: Johan Brandhorst-Satzkorn --- docs/resources/worker.md | 39 +++ examples/resources/boundary_worker/import.sh | 1 + examples/resources/boundary_worker/main.tf | 13 + internal/provider/provider.go | 1 + internal/provider/worker.go | 239 +++++++++++++++++++ internal/provider/worker_test.go | 180 ++++++++++++++ 6 files changed, 473 insertions(+) create mode 100644 docs/resources/worker.md create mode 100644 examples/resources/boundary_worker/import.sh create mode 100644 examples/resources/boundary_worker/main.tf create mode 100644 internal/provider/worker.go create mode 100644 internal/provider/worker_test.go diff --git a/docs/resources/worker.md b/docs/resources/worker.md new file mode 100644 index 00000000..1b7cdc01 --- /dev/null +++ b/docs/resources/worker.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_worker Resource - terraform-provider-boundary" +subcategory: "" +description: |- + The resource allows you to create a self-managed worker object. +--- + +# boundary_worker (Resource) + +The resource allows you to create a self-managed worker object. + + + + +## Schema + +### Required + +- `scope_id` (String) The scope for the worker. + +### Optional + +- `api_tags` (Map of List of String) API tags applied to the worker. +- `description` (String) The description for the worker. +- `name` (String) The name for the worker. +- `worker_generated_auth_token` (String) The worker authentication token required to register the worker for the worker-led authentication flow. Leaving this blank will result in a controller generated token. + +### Read-Only + +- `address` (String) The accessible address of the self managed worker. +- `authorized_actions` (List of String) A list of actions that the worker is entitled to perform. +- `canonical_tags` (Map of List of String) The aggregated view of worker tags and API tags. +- `config_tags` (Map of List of String) Tags as configured in the worker's HCL file. +- `controller_generated_activation_token` (String) A single use token generated by the controller to be passed to the self-managed worker. +- `id` (String) The ID of the worker. +- `release_version` (Number) The version of the Boundary binary running on the self managed worker. + + diff --git a/examples/resources/boundary_worker/import.sh b/examples/resources/boundary_worker/import.sh new file mode 100644 index 00000000..28c41614 --- /dev/null +++ b/examples/resources/boundary_worker/import.sh @@ -0,0 +1 @@ +terraform import boundary_worker.foo \ No newline at end of file diff --git a/examples/resources/boundary_worker/main.tf b/examples/resources/boundary_worker/main.tf new file mode 100644 index 00000000..a099f9d1 --- /dev/null +++ b/examples/resources/boundary_worker/main.tf @@ -0,0 +1,13 @@ +resource "boundary_worker" "controller_led" { + scope_id = "global" + name = "worker 1" + description = "self managed worker with controlled led auth" + worker_generated_auth_token = var.worker_generated_auth_token +} + +resource "boundary_self_managed_worker" "worker_led" { + scope_id = "global" + name = "worker 2" + description = "self managed worker with controlled led auth" + worker_generated_auth_token = var.worker_generated_auth_token +} \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 1887d2b6..b784a7aa 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -94,6 +94,7 @@ func New() *schema.Provider { "boundary_scope": resourceScope(), "boundary_target": resourceTarget(), "boundary_user": resourceUser(), + "boundary_worker": resourceWorker(), }, } diff --git a/internal/provider/worker.go b/internal/provider/worker.go new file mode 100644 index 00000000..b7925c9b --- /dev/null +++ b/internal/provider/worker.go @@ -0,0 +1,239 @@ +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/workers" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + scope = "scope" + scopeId = "global" + version = "version" + address = "address" + canonicalTags = "canonical_tags" + configTags = "config_tags" + workerGeneratedAuthToken = "worker_generated_auth_token" + controllerGeneratedActivationToken = "controller_generated_activation_token" + apiTags = "api_tags" + releaseVersion = "release_version" + authorizedActions = "authorized_actions" +) + +func resourceWorker() *schema.Resource { + return &schema.Resource{ + Description: "The resource allows you to create a self-managed worker object.", + + CreateContext: resourceWorkerCreate, + ReadContext: resourceWorkerRead, + UpdateContext: resourceWorkerUpdate, + DeleteContext: resourceWorkerDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of the worker.", + Type: schema.TypeString, + Computed: true, + }, + ScopeIdKey: { + Description: "The scope for the worker.", + Type: schema.TypeString, + Required: true, + }, + NameKey: { + Description: "The name for the worker.", + Type: schema.TypeString, + Optional: true, + }, + DescriptionKey: { + Description: "The description for the worker.", + Type: schema.TypeString, + Optional: true, + }, + address: { + Description: "The accessible address of the self managed worker.", + Type: schema.TypeString, + Computed: true, + }, + workerGeneratedAuthToken: { + Description: "The worker authentication token required to register the worker for the worker-led authentication flow. Leaving this blank will result in a controller generated token.", + Type: schema.TypeString, + Optional: true, + }, + controllerGeneratedActivationToken: { + Description: "A single use token generated by the controller to be passed to the self-managed worker.", + Type: schema.TypeString, + Computed: true, + }, + releaseVersion: { + Description: "The version of the Boundary binary running on the self managed worker.", + Type: schema.TypeInt, + 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, + }, + }, + } +} + +func setFromWorkerResponseMap(d *schema.ResourceData, raw map[string]interface{}) error { + d.SetId(raw["id"].(string)) + d.Set(ScopeIdKey, raw["scope_id"]) + d.Set(NameKey, raw["name"]) + d.Set(DescriptionKey, raw["description"]) + d.Set(address, raw["address"]) + d.Set(workerGeneratedAuthToken, raw["worker_generated_auth_token"]) + d.Set(controllerGeneratedActivationToken, raw["controller_generated_activation_token"]) + d.Set(releaseVersion, raw["release_version"]) + d.Set(authorizedActions, raw["authorized_actions"]) + + return nil +} + +func resourceWorkerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + wkrs := workers.NewClient(md.client) + + wrr, err := wkrs.Read(ctx, d.Id()) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error calling read worker: %v", err) + } + if wrr == nil { + return diag.Errorf("worker nil after read") + } + + if err := setFromWorkerResponseMap(d, wrr.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceWorkerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + opts := []workers.Option{} + + if v, ok := d.GetOk(NameKey); ok { + opts = append(opts, workers.WithName(v.(string))) + } + + if v, ok := d.GetOk(DescriptionKey); ok { + opts = append(opts, workers.WithDescription(v.(string))) + } + + var workerAuthToken string + if v, ok := d.GetOk(workerGeneratedAuthToken); ok { + workerAuthToken = v.(string) + } + + wkr := workers.NewClient(md.client) + + if len(workerAuthToken) > 0 { + wkrc, err := wkr.CreateWorkerLed(ctx, workerAuthToken, scopeId, opts...) + if err != nil { + return diag.Errorf("error creating worker: %v", err) + } + if wkrc == nil { + return diag.Errorf("worker nil after create") + } + if err := setFromWorkerResponseMap(d, wkrc.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + } else { + wkrc, err := wkr.CreateControllerLed(ctx, scopeId, opts...) + if err != nil { + return diag.Errorf("error creating worker: %v", err) + } + if wkrc == nil { + return diag.Errorf("worker nil after create") + } + if err := setFromWorkerResponseMap(d, wkrc.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + } + return nil +} + +func resourceWorkerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + wkr := workers.NewClient(md.client) + + opts := []workers.Option{} + + var name *string + if d.HasChange(NameKey) { + opts = append(opts, workers.DefaultName()) + nameVal, ok := d.GetOk(NameKey) + if ok { + nameStr := nameVal.(string) + name = &nameStr + opts = append(opts, workers.WithName(nameStr)) + } + } + + var desc *string + if d.HasChange(DescriptionKey) { + opts = append(opts, workers.DefaultDescription()) + descVal, ok := d.GetOk(DescriptionKey) + if ok { + descStr := descVal.(string) + desc = &descStr + opts = append(opts, workers.WithDescription(descStr)) + } + } + + var versionInt int + if versionVal, ok := d.GetOk(version); ok { + versionInt = versionVal.(int) + } + + if len(opts) > 0 { + opts = append(opts, workers.WithAutomaticVersioning(true)) + _, err := wkr.Update(ctx, d.Id(), uint32(versionInt), opts...) + if err != nil { + return diag.Errorf("error updating worker: %v", err) + } + } + + if d.HasChange(NameKey) { + if err := d.Set(NameKey, name); err != nil { + return diag.FromErr(err) + } + } + if d.HasChange(DescriptionKey) { + if err := d.Set(DescriptionKey, desc); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceWorkerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + wClient := workers.NewClient(md.client) + + _, err := wClient.Delete(ctx, d.Id()) + if err != nil { + return diag.Errorf("error deleting worker: %v", err) + } + + return nil +} diff --git a/internal/provider/worker_test.go b/internal/provider/worker_test.go new file mode 100644 index 00000000..78bb03fb --- /dev/null +++ b/internal/provider/worker_test.go @@ -0,0 +1,180 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "os" + "testing" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/workers" + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + workerName = "self managed worker" + workerNameUpdate = "self managed worker update" + workerDesc = "self managed worker description" + workerDescUpdate = "self managed worker description update" + workerToken = "GzusqckarbczHoLGQ4UA25uSRGy7e7hUk4Qgz8T3NmSZF4oXBc1dKMRyH7Mr6W5u2QWggkZi1wsnfYMufb5LZJJnxEdywUxpE8RgAbVTahMhJm8oeH3nagrAKfWJkrTt8QuoqiupcTrFmJtDPZ5WBtqKSXiW3jdG8esCt7ZNKVaKV1Hq6MBXUf7duj9KfAy4Y3B31jDTzoF1uVnK1AsaLkEhgbHbuAH33L2KG5ivo7YeFeE6PknNoVavPSRSkoEpcXSfjvoPMAz9ttpNvH7jGWPwLti8r48NcVj41ftXWg" +) + +var ( + workerLedCreate = fmt.Sprintf(` + resource "boundary_worker" "worker_led" { + scope_id = "global" + name = "%s" + description = "%s" + worker_generated_auth_token = "%s" + }`, workerName, workerDesc, workerToken) + + workerLedUpdate = fmt.Sprintf(` + resource "boundary_worker" "worker_led" { + scope_id = "global" + name = "%s" + description = "%s" + worker_generated_auth_token = "%s" + }`, workerNameUpdate, workerDescUpdate, workerToken) + + controllerLedCreate = fmt.Sprintf(` +resource "boundary_worker" "controller_led" { + scope_id = "global" + name = "%s" + description = "%s" +}`, workerName, workerDesc) + controllerLedUpdate = fmt.Sprintf(` +resource "boundary_worker" "controller_led" { + scope_id = "global" + name = "%s" + description = "%s" +}`, workerNameUpdate, workerDescUpdate) +) + +func TestWorkerWorkerLed(t *testing.T) { + token := os.Getenv("BOUNDARY_TF_PROVIDER_TEST_WORKER_LED_TOKEN") + if token == "" { + t.Skip("Not running worker led activation test without worker led token present") + } + + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckworkerResourceDestroy(t, provider), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, workerLedCreate), + Check: resource.ComposeTestCheckFunc( + testAccCheckworkerResourceExists(provider, "boundary_worker.worker_led"), + resource.TestCheckResourceAttr("boundary_worker.worker_led", "description", workerDesc), + resource.TestCheckResourceAttr("boundary_worker.worker_led", "name", workerName), + ), + }, + importStep("boundary_worker.worker_led"), + { + // update + Config: testConfig(url, workerLedUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckworkerResourceExists(provider, "boundary_worker.worker_led"), + resource.TestCheckResourceAttr("boundary_worker.worker_led", "description", workerDescUpdate), + resource.TestCheckResourceAttr("boundary_worker.worker_led", "name", workerNameUpdate), + ), + }, + importStep("boundary_worker.worker_led"), + }, + }) +} + +func TestWorkerControllerLed(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckworkerResourceDestroy(t, provider), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, controllerLedCreate), + Check: resource.ComposeTestCheckFunc( + testAccCheckworkerResourceExists(provider, "boundary_worker.controller_led"), + resource.TestCheckResourceAttr("boundary_worker.controller_led", "description", workerDesc), + resource.TestCheckResourceAttr("boundary_worker.controller_led", "name", workerName), + resource.TestCheckResourceAttrSet("boundary_worker.controller_led", "controller_generated_activation_token"), + ), + }, + importStep("boundary_worker.controller_led"), + { + // update + Config: testConfig(url, controllerLedUpdate), + Check: resource.ComposeTestCheckFunc( + testAccCheckworkerResourceExists(provider, "boundary_worker.controller_led"), + resource.TestCheckResourceAttr("boundary_worker.controller_led", "description", workerDescUpdate), + resource.TestCheckResourceAttr("boundary_worker.controller_led", "name", workerNameUpdate), + ), + }, + importStep("boundary_worker.controller_led"), + }, + }) +} + +func testAccCheckworkerResourceExists(testProvider *schema.Provider, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("No ID is set") + } + + md := testProvider.Meta().(*metaData) + + wkrClient := workers.NewClient(md.client) + + if _, err := wkrClient.Read(context.Background(), id); err != nil { + return fmt.Errorf("Got an error when reading worker %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckworkerResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + if testProvider.Meta() == nil { + t.Fatal("got nil provider metadata") + } + md := testProvider.Meta().(*metaData) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "boundary_worker": + id := rs.Primary.ID + + wkrClient := workers.NewClient(md.client) + + _, err := wkrClient.Read(context.Background(), id) + if apiErr := api.AsServerError(err); apiErr == nil || apiErr.Response().StatusCode() != http.StatusNotFound { + return fmt.Errorf("didn't get a 404 when reading destroyed worker %q: %v", id, err) + } + + default: + continue + } + } + return nil + } +}