From 382deed9d35f695ae328a4800ceb45195e3dfdd6 Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 7 Jul 2023 17:23:43 -0400 Subject: [PATCH] add resource and data sources for node pools --- go.mod | 3 +- go.sum | 8 +- nomad/data_source_node_pool.go | 88 +++++++ nomad/data_source_node_pool_test.go | 148 +++++++++++ nomad/data_source_node_pools.go | 120 +++++++++ nomad/data_source_node_pools_test.go | 93 +++++++ nomad/provider.go | 3 + nomad/resource_node_pool.go | 254 +++++++++++++++++++ nomad/resource_node_pool_test.go | 318 ++++++++++++++++++++++++ website/docs/d/node_pool.html.markdown | 41 +++ website/docs/d/node_pools.html.markdown | 51 ++++ website/docs/r/node_pool.html.markdown | 48 ++++ website/nomad.erb | 13 +- 13 files changed, 1180 insertions(+), 8 deletions(-) create mode 100644 nomad/data_source_node_pool.go create mode 100644 nomad/data_source_node_pool_test.go create mode 100644 nomad/data_source_node_pools.go create mode 100644 nomad/data_source_node_pools_test.go create mode 100644 nomad/resource_node_pool.go create mode 100644 nomad/resource_node_pool_test.go create mode 100644 website/docs/d/node_pool.html.markdown create mode 100644 website/docs/d/node_pools.html.markdown create mode 100644 website/docs/r/node_pool.html.markdown diff --git a/go.mod b/go.mod index 5c17b14f..fce0cb8f 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/nomad v1.5.2 - github.com/hashicorp/nomad/api v0.0.0-20230321225438-9a2fdb5f53dc + github.com/hashicorp/nomad/api v0.0.0-20230627160105-1d1e606846fa // v1.6.0-beta.1 github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 github.com/hashicorp/vault v0.10.4 github.com/stretchr/testify v1.8.1 @@ -78,7 +78,6 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect - github.com/shoenig/test v0.6.3 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect diff --git a/go.sum b/go.sum index 316ad9a6..96423d95 100644 --- a/go.sum +++ b/go.sum @@ -105,6 +105,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= @@ -238,8 +239,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/nomad v1.5.2 h1:4dHbKF2UZEYXoXPUfG1VdSDaOaGpuqZUiCnuKuna83o= github.com/hashicorp/nomad v1.5.2/go.mod h1:ywYvhApBI9mRlmS8qMhoaWUbzeVGiJHKMjwy8l7fHRY= -github.com/hashicorp/nomad/api v0.0.0-20230321225438-9a2fdb5f53dc h1:XhUHxwOMmGbSun7xsszFLsBqGz3WbWhi5zWVAEo+c8M= -github.com/hashicorp/nomad/api v0.0.0-20230321225438-9a2fdb5f53dc/go.mod h1:bKUb1ytds5KwUioHdvdq9jmrDqCThv95si0Ub7iNeBg= +github.com/hashicorp/nomad/api v0.0.0-20230627160105-1d1e606846fa h1:RzT6JRS4Ehe12Iy2kmsCrjivgsHuGngCXEwxoirNz9U= +github.com/hashicorp/nomad/api v0.0.0-20230627160105-1d1e606846fa/go.mod h1:Xjd3OXUTfsWbCCBsQd3EdfPTz5evDi+fxqdvpN+WqQg= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/terraform-exec v0.15.0 h1:cqjh4d8HYNQrDoEmlSGelHmg2DYDh5yayckvJ5bV18E= github.com/hashicorp/terraform-exec v0.15.0/go.mod h1:H4IG8ZxanU+NW0ZpDRNsvh9f0ul7C0nHP+rUR/CHs7I= @@ -356,8 +357,7 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c= -github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= diff --git a/nomad/data_source_node_pool.go b/nomad/data_source_node_pool.go new file mode 100644 index 00000000..49d79666 --- /dev/null +++ b/nomad/data_source_node_pool.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package nomad + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-nomad/nomad/helper" +) + +func dataSourceNodePool() *schema.Resource { + return &schema.Resource{ + Read: dataSourceNodePoolRead, + + Schema: map[string]*schema.Schema{ + "name": { + Description: "Unique name for this node pool.", + Type: schema.TypeString, + Required: true, + }, + "description": { + Description: "Description for this node pool.", + Type: schema.TypeString, + Computed: true, + }, + "meta": { + Description: "Metadata associated with the node pool", + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "scheduler_config": { + Description: "Scheduler configuration for the node pool.", + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "scheduler_algorithm": { + Description: "The scheduler algorithm to use in the node pool.", + Type: schema.TypeString, + Computed: true, + }, + + // This field must be a string instead of a bool (and differ from + // Nomad) in order to represent a tristate. + // - "enabled": memory oversubscription is enabled in the pool. + // - "disabled": memory oversubscription is disabled in the pool. + // - "": the global memory oversubscription value is used. + "memory_oversubscription": { + Description: "If true, the node pool will have memory oversubscription enabled.", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Computed: true, + }, + }, + } +} + +func dataSourceNodePoolRead(d *schema.ResourceData, meta any) error { + client := meta.(ProviderConfig).client + + name := d.Get("name").(string) + log.Printf("[DEBUG] Reading node pool %q", name) + pool, _, err := client.NodePools().Info(name, nil) + if err != nil { + return fmt.Errorf("error reading node pool %q: %w", name, err) + } + log.Printf("[DEBUG] Read node pool %q", name) + + sw := helper.NewStateWriter(d) + sw.Set("name", pool.Name) + sw.Set("description", pool.Description) + sw.Set("meta", pool.Meta) + sw.Set("scheduler_config", flattenNodePoolSchedulerConfiguration(pool.SchedulerConfiguration)) + if err := sw.Error(); err != nil { + return err + } + + d.SetId(name) + return nil +} diff --git a/nomad/data_source_node_pool_test.go b/nomad/data_source_node_pool_test.go new file mode 100644 index 00000000..13fcd238 --- /dev/null +++ b/nomad/data_source_node_pool_test.go @@ -0,0 +1,148 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package nomad + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestDataSourceNodePool(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1") }, + Steps: []resource.TestStep{ + { + Config: testDataSourceNodePoolConfig_builtIn, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nomad_node_pool.all", "name", "all"), + resource.TestCheckResourceAttr("data.nomad_node_pool.default", "name", "default"), + ), + }, + { + Config: testDataSourceNodePoolConfig_doesntExist, + ExpectError: regexp.MustCompile("node pool not found"), + }, + { + Config: testDataSourceNodePoolConfig_basic(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nomad_node_pool.test", "name", name), + resource.TestCheckResourceAttr("data.nomad_node_pool.test", "description", "Terraform test node pool"), + resource.TestCheckResourceAttr("data.nomad_node_pool.test", "meta.%", "1"), + resource.TestCheckResourceAttr("data.nomad_node_pool.test", "meta.test", "true"), + resource.TestCheckNoResourceAttr("data.nomad_node_pool.test", "scheduler_config"), + ), + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +func TestDataSourceNodePool_schedConfig(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1"); testCheckEnt(t) }, + Steps: []resource.TestStep{ + { + Config: testDataSourceNodePoolConfig_schedConfig(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nomad_node_pool.no_mem_oversub", "name", fmt.Sprintf("%s-no-mem-oversub", name)), + resource.TestCheckResourceAttr("data.nomad_node_pool.no_mem_oversub", "scheduler_config.0.scheduler_algorithm", "spread"), + resource.TestCheckResourceAttr("data.nomad_node_pool.no_mem_oversub", "scheduler_config.0.memory_oversubscription", ""), + + resource.TestCheckResourceAttr("data.nomad_node_pool.mem_oversub_disabled", "name", fmt.Sprintf("%s-mem-oversub-disabled", name)), + resource.TestCheckResourceAttr("data.nomad_node_pool.mem_oversub_disabled", "scheduler_config.0.scheduler_algorithm", "binpack"), + resource.TestCheckResourceAttr("data.nomad_node_pool.mem_oversub_disabled", "scheduler_config.0.memory_oversubscription", "disabled"), + + resource.TestCheckResourceAttr("data.nomad_node_pool.mem_oversub_enabled", "name", fmt.Sprintf("%s-mem-oversub-enabled", name)), + resource.TestCheckResourceAttr("data.nomad_node_pool.mem_oversub_enabled", "scheduler_config.0.scheduler_algorithm", "binpack"), + resource.TestCheckResourceAttr("data.nomad_node_pool.mem_oversub_enabled", "scheduler_config.0.memory_oversubscription", "enabled"), + ), + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +const testDataSourceNodePoolConfig_builtIn = ` +data "nomad_node_pool" "all" { + name = "all" +} + +data "nomad_node_pool" "default" { + name = "default" +} +` + +const testDataSourceNodePoolConfig_doesntExist = ` +data "nomad_node_pool" "doesnt_exist" { + name = "doesnt-exist" +} +` + +func testDataSourceNodePoolConfig_basic(name string) string { + return fmt.Sprintf(` +resource "nomad_node_pool" "test" { + name = "%s" + description = "Terraform test node pool" + + meta = { + test = "true" + } +} + +data "nomad_node_pool" "test" { + name = nomad_node_pool.test.name +} +`, name) +} + +func testDataSourceNodePoolConfig_schedConfig(prefix string) string { + return fmt.Sprintf(` +resource "nomad_node_pool" "no_mem_oversub" { + name = "%[1]s-no-mem-oversub" + + scheduler_config { + scheduler_algorithm = "spread" + } +} + +data "nomad_node_pool" "no_mem_oversub" { + name = nomad_node_pool.no_mem_oversub.name +} + + +resource "nomad_node_pool" "mem_oversub_disabled" { + name = "%[1]s-mem-oversub-disabled" + + scheduler_config { + scheduler_algorithm = "binpack" + memory_oversubscription = "disabled" + } +} + +data "nomad_node_pool" "mem_oversub_disabled" { + name = nomad_node_pool.mem_oversub_disabled.name +} + +resource "nomad_node_pool" "mem_oversub_enabled" { + name = "%[1]s-mem-oversub-enabled" + + scheduler_config { + scheduler_algorithm = "binpack" + memory_oversubscription = "enabled" + } +} + +data "nomad_node_pool" "mem_oversub_enabled" { + name = nomad_node_pool.mem_oversub_enabled.name +} +`, prefix) +} diff --git a/nomad/data_source_node_pools.go b/nomad/data_source_node_pools.go new file mode 100644 index 00000000..082418a4 --- /dev/null +++ b/nomad/data_source_node_pools.go @@ -0,0 +1,120 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package nomad + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceNodePools() *schema.Resource { + return &schema.Resource{ + Read: dataSourceNodePoolsRead, + + Schema: map[string]*schema.Schema{ + "prefix": { + Description: "Specifies a string to filter node pools based on a name prefix.", + Type: schema.TypeString, + Optional: true, + }, + "filter": { + Description: "Specifies the expression used to filter the results.", + Type: schema.TypeString, + Optional: true, + }, + "node_pools": { + Description: "List of node pools returned", + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Unique name for this node pool.", + Type: schema.TypeString, + Computed: true, + }, + "description": { + Description: "Description for this node pool.", + Type: schema.TypeString, + Computed: true, + }, + "meta": { + Description: "Metadata associated with the node pool", + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "scheduler_config": { + Description: "Scheduler configuration for the node pool.", + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "scheduler_algorithm": { + Description: "The scheduler algorithm to use in the node pool.", + Type: schema.TypeString, + Computed: true, + }, + + // This field must be a string instead of a bool (and differ from + // Nomad) in order to represent a tristate. + // - "enabled": memory oversubscription is enabled in the pool. + // - "disabled": memory oversubscription is disabled in the pool. + // - "": the global memory oversubscription value is used. + "memory_oversubscription": { + Description: "If true, the node pool will have memory oversubscription enabled.", + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Computed: true, + }, + }, + }, + Computed: true, + }, + }, + } +} + +func dataSourceNodePoolsRead(d *schema.ResourceData, meta any) error { + client := meta.(ProviderConfig).client + + prefix := d.Get("prefix").(string) + filter := d.Get("filter").(string) + id := strconv.Itoa(schema.HashString(prefix + filter)) + + log.Printf("[DEBUG] Reading node pool list") + resp, _, err := client.NodePools().List(&api.QueryOptions{ + Prefix: prefix, + Filter: filter, + }) + if err != nil { + if strings.Contains(err.Error(), "404") { + d.SetId("") + return nil + } + return fmt.Errorf("error reading node pools: %w", err) + } + + pools := make([]map[string]any, len(resp)) + for i, p := range resp { + pools[i] = map[string]any{ + "name": p.Name, + "description": p.Description, + "meta": p.Meta, + "scheduler_config": flattenNodePoolSchedulerConfiguration(p.SchedulerConfiguration), + } + } + log.Printf("[DEBUG] Read node pool list") + + d.SetId(id) + return d.Set("node_pools", pools) +} diff --git a/nomad/data_source_node_pools_test.go b/nomad/data_source_node_pools_test.go new file mode 100644 index 00000000..50b84581 --- /dev/null +++ b/nomad/data_source_node_pools_test.go @@ -0,0 +1,93 @@ +package nomad + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestDataSourceNodePools_basic(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1") }, + Steps: []resource.TestStep{ + { + Config: testDataSourceNodePools_basic(name), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nomad_node_pools.all_pools", "node_pools.#", "5"), + resource.TestCheckResourceAttr("data.nomad_node_pools.prefix", "node_pools.#", "2"), + resource.TestCheckResourceAttr("data.nomad_node_pools.filter", "node_pools.#", "2"), + resource.TestCheckResourceAttr("data.nomad_node_pools.filter_with_prefix", "node_pools.#", "1"), + ), + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +func testDataSourceNodePools_basic(prefix string) string { + return fmt.Sprintf(` +resource "nomad_node_pool" "basic" { + name = "%[1]s-basic" + description = "Terraform test node pool" + + meta = { + test = "%[1]s" + } +} + +resource "nomad_node_pool" "simple" { + name = "%[1]s-simple" +} + +resource "nomad_node_pool" "different_prefix" { + name = "other-%[1]s" + + meta = { + test = "%[1]s" + } +} + +data "nomad_node_pools" "all_pools" { + depends_on = [ + nomad_node_pool.basic, + nomad_node_pool.simple, + nomad_node_pool.different_prefix, + ] +} + +data "nomad_node_pools" "prefix" { + depends_on = [ + nomad_node_pool.basic, + nomad_node_pool.simple, + nomad_node_pool.different_prefix, + ] + + prefix = "%[1]s" +} + +data "nomad_node_pools" "filter" { + depends_on = [ + nomad_node_pool.basic, + nomad_node_pool.simple, + nomad_node_pool.different_prefix, + ] + + filter = "Meta.test == \"%[1]s\"" +} + +data "nomad_node_pools" "filter_with_prefix" { + depends_on = [ + nomad_node_pool.basic, + nomad_node_pool.simple, + nomad_node_pool.different_prefix, + ] + + prefix = "%[1]s" + filter = "Meta.test == \"%[1]s\"" +} +`, prefix) +} diff --git a/nomad/provider.go b/nomad/provider.go index 882e559f..032262cf 100644 --- a/nomad/provider.go +++ b/nomad/provider.go @@ -152,6 +152,8 @@ func Provider() *schema.Provider { "nomad_job_parser": dataSourceJobParser(), "nomad_namespace": dataSourceNamespace(), "nomad_namespaces": dataSourceNamespaces(), + "nomad_node_pool": dataSourceNodePool(), + "nomad_node_pools": dataSourceNodePools(), "nomad_plugin": dataSourcePlugin(), "nomad_plugins": dataSourcePlugins(), "nomad_scaling_policies": dataSourceScalingPolicies(), @@ -171,6 +173,7 @@ func Provider() *schema.Provider { "nomad_external_volume": resourceExternalVolume(), "nomad_job": resourceJob(), "nomad_namespace": resourceNamespace(), + "nomad_node_pool": resourceNodePool(), "nomad_quota_specification": resourceQuotaSpecification(), "nomad_sentinel_policy": resourceSentinelPolicy(), "nomad_volume": resourceVolume(), diff --git a/nomad/resource_node_pool.go b/nomad/resource_node_pool.go new file mode 100644 index 00000000..5bfc1e47 --- /dev/null +++ b/nomad/resource_node_pool.go @@ -0,0 +1,254 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package nomad + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-nomad/nomad/helper" + "github.com/hashicorp/terraform-provider-nomad/nomad/helper/pointer" +) + +const ( + nodePoolMemoryOversubscriptionEnabled = "enabled" + nodePoolMemoryOversubscriptionDisabled = "disabled" +) + +func resourceNodePool() *schema.Resource { + return &schema.Resource{ + Create: resourceNodePoolWrite, + Update: resourceNodePoolWrite, + Delete: resourceNodePoolDelete, + Read: resourceNodePoolRead, + Exists: resourceNodePoolExists, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Description: "Unique name for this node pool.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "description": { + Description: "Description for this node pool.", + Type: schema.TypeString, + Optional: true, + }, + "meta": { + Description: "Metadata associated with the node pool", + Type: schema.TypeMap, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + }, + "scheduler_config": { + Description: "Scheduler configuration for the node pool.", + Type: schema.TypeList, + Elem: resourceNodePoolSchedulerConfiguration(), + MaxItems: 1, + Optional: true, + }, + }, + } +} + +func resourceNodePoolSchedulerConfiguration() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "scheduler_algorithm": { + Description: "The scheduler algorithm to use in the node pool.", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(api.SchedulerAlgorithmBinpack), + string(api.SchedulerAlgorithmSpread), + }, false), + }, + + // This field must be a string instead of a bool (and differ from + // Nomad) in order to represent a tristate. + // - "enabled": memory oversubscription is enabled in the pool. + // - "disabled": memory oversubscription is disabled in the pool. + // - "": the global memory oversubscription value is used. + "memory_oversubscription": { + Description: "If true, the node pool will have memory oversubscription enabled.", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + nodePoolMemoryOversubscriptionEnabled, + nodePoolMemoryOversubscriptionDisabled, + }, false), + }, + }, + } + +} + +func resourceNodePoolRead(d *schema.ResourceData, meta any) error { + client := meta.(ProviderConfig).client + name := d.Id() + + log.Printf("[DEBUG] Reading node pool %q", name) + pool, _, err := client.NodePools().Info(name, nil) + if err != nil { + // we have Exists, so no need to handle 404 + return fmt.Errorf("error reading node pool %q: %w", name, err) + } + log.Printf("[DEBUG] Read node pool %q", name) + + sw := helper.NewStateWriter(d) + sw.Set("name", pool.Name) + sw.Set("description", pool.Description) + sw.Set("meta", pool.Meta) + sw.Set("scheduler_config", flattenNodePoolSchedulerConfiguration(pool.SchedulerConfiguration)) + + return sw.Error() +} + +func resourceNodePoolWrite(d *schema.ResourceData, meta any) error { + client := meta.(ProviderConfig).client + + m := make(map[string]string) + for name, value := range d.Get("meta").(map[string]any) { + m[name] = value.(string) + } + + schedConfig, err := expandNodePoolSchedulerConfiguration(d) + if err != nil { + return fmt.Errorf("failed to parse node pool scheduler configuration: %w", err) + } + + pool := &api.NodePool{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Meta: m, + SchedulerConfiguration: schedConfig, + } + + log.Printf("[DEBUG] Upserting node pool %q", pool.Name) + if _, err := client.NodePools().Register(pool, nil); err != nil { + return fmt.Errorf("error upserting node pool %q: %w", pool.Name, err) + } + log.Printf("[DEBUG] Upserted node pool %q", pool.Name) + d.SetId(pool.Name) + + return resourceNodePoolRead(d, meta) +} + +func resourceNodePoolDelete(d *schema.ResourceData, meta any) error { + client := meta.(ProviderConfig).client + name := d.Id() + + log.Printf("[DEBUG] Deleting node pool %q", name) + retries := 0 + for { + _, err := client.NodePools().Delete(name, nil) + if err == nil { + break + } + + retry := strings.Contains(err.Error(), "has non-terminal jobs") || + strings.Contains(err.Error(), "has nodes") + if retries < 10 && retry { + log.Printf("[INFO] could not delete node pool %q, retrying: %v", name, err) + time.Sleep(5 * time.Second) + retries++ + continue + } + + return fmt.Errorf("failed to delete node pool %q: %w", name, err) + } + log.Printf("[DEBUG] Deleted node pool %q", name) + + return nil +} + +func resourceNodePoolExists(d *schema.ResourceData, meta any) (bool, error) { + client := meta.(ProviderConfig).client + + name := d.Id() + log.Printf("[DEBUG] Checking if node pool %q exists", name) + resp, _, err := client.NodePools().Info(name, nil) + if err != nil { + if strings.Contains(err.Error(), "404") { + return false, nil + } + + return true, fmt.Errorf("error checking for node pool %q: %#v", name, err) + } + if resp == nil { + // just to be sure + log.Printf("[DEBUG] Response was nil, node pool %q doesn't exist", name) + return false, nil + } + + return true, nil +} + +func expandNodePoolSchedulerConfiguration(d *schema.ResourceData) (*api.NodePoolSchedulerConfiguration, error) { + rawConfig := d.Get("scheduler_config").([]any) + if len(rawConfig) < 1 { + return nil, nil + } + + config, ok := rawConfig[0].(map[string]any) + if !ok { + return nil, fmt.Errorf("unexpected type %T for node pool scheduler configuration", rawConfig[0]) + } + + result := &api.NodePoolSchedulerConfiguration{} + + if rawAlgo, ok := config["scheduler_algorithm"]; ok { + algo, ok := rawAlgo.(string) + if !ok { + return nil, fmt.Errorf("unexpected type %T for scheduler algorithm", rawAlgo) + } + result.SchedulerAlgorithm = api.SchedulerAlgorithm(algo) + } + + if rawMemOverSub, ok := config["memory_oversubscription"]; ok { + memOverSub, ok := rawMemOverSub.(string) + if !ok { + return nil, fmt.Errorf("unexpected type %T for memory oversubcription enabled", rawMemOverSub) + } + switch memOverSub { + case nodePoolMemoryOversubscriptionEnabled: + result.MemoryOversubscriptionEnabled = pointer.Of(true) + case nodePoolMemoryOversubscriptionDisabled: + result.MemoryOversubscriptionEnabled = pointer.Of(false) + } + } + return result, nil +} + +func flattenNodePoolSchedulerConfiguration(config *api.NodePoolSchedulerConfiguration) []any { + if config == nil { + return nil + } + + rawConfig := map[string]any{ + "scheduler_algorithm": string(config.SchedulerAlgorithm), + } + + if config.MemoryOversubscriptionEnabled != nil { + if *config.MemoryOversubscriptionEnabled { + rawConfig["memory_oversubscription"] = nodePoolMemoryOversubscriptionEnabled + } else { + rawConfig["memory_oversubscription"] = nodePoolMemoryOversubscriptionDisabled + } + } + + return []any{rawConfig} +} diff --git a/nomad/resource_node_pool_test.go b/nomad/resource_node_pool_test.go new file mode 100644 index 00000000..f2ac8025 --- /dev/null +++ b/nomad/resource_node_pool_test.go @@ -0,0 +1,318 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package nomad + +import ( + "errors" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestResourceNodePool_import(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1") }, + Steps: []resource.TestStep{ + { + Config: testResourceNodePoolConfig_basic(name), + Check: testResourceNodePoolCheck_basic(name), + }, + { + ResourceName: "nomad_node_pool.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +func TestResourceNodePool_schedulerConfig(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1"); testCheckEnt(t) }, + Steps: []resource.TestStep{ + { + Config: testResourceNodePoolConfig_schedConfig(name), + Check: testResourceNodePoolCheck_schedConfig(name), + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +func TestResourceNodePool_refresh(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1") }, + Steps: []resource.TestStep{ + { + Config: testResourceNodePoolConfig_basic(name), + Check: testResourceNodePoolCheck_basic(name), + }, + + // This should successfully cause the policy to be recreated, + // testing the Exists function. + { + PreConfig: testResourceNodePool_delete(t, name), + Config: testResourceNodePoolConfig_basic(name), + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +func TestResourceNodePool_update(t *testing.T) { + name := acctest.RandomWithPrefix("tf-nomad-test") + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1"); testCheckEnt(t) }, + Steps: []resource.TestStep{ + { + Config: testResourceNodePoolConfig_schedConfig(name), + Check: testResourceNodePoolCheck_schedConfig(name), + }, + { + Config: testResourceNodePoolConfig_updated(name), + Check: testResourceNodePoolCheck_updated(name), + }, + }, + CheckDestroy: testResourceNodePool_checkDestroy(name), + }) +} + +func testResourceNodePoolConfig_basic(name string) string { + return fmt.Sprintf(` +resource "nomad_node_pool" "test" { + name = "%s" + description = "Terraform test node pool" + + meta = { + test = "true" + } +} +`, name) +} + +func testResourceNodePoolCheck_basic(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["nomad_node_pool.test"] + if resourceState == nil { + return errors.New("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return errors.New("resource has no primary instance") + } + + if instanceState.ID != name { + return fmt.Errorf("expected resource ID to be %q, got %q", name, instanceState.ID) + } + + client := testProvider.Meta().(ProviderConfig).client + pool, _, err := client.NodePools().Info(name, nil) + if err != nil { + return fmt.Errorf("error fetching node pool %q: %v", name, err) + } + + if pool.Name != name { + return fmt.Errorf("expected name to be %q, got %q", name, pool.Name) + } + + expectedDescription := "Terraform test node pool" + if pool.Description != expectedDescription { + return fmt.Errorf("expected description to be %q, got %q", expectedDescription, pool.Description) + } + + expectedMeta := map[string]string{ + "test": "true", + } + if diff := cmp.Diff(pool.Meta, expectedMeta); diff != "" { + return fmt.Errorf("meta mismatch (-want +got):\n%s", diff) + } + + return nil + } +} + +func testResourceNodePoolConfig_schedConfig(name string) string { + return fmt.Sprintf(` +resource "nomad_node_pool" "test" { + name = "%s" + description = "Terraform test node pool" + + meta = { + test = "true" + } + + scheduler_config { + scheduler_algorithm = "spread" + memory_oversubscription = "enabled" + } +} +`, name) +} + +func testResourceNodePoolCheck_schedConfig(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["nomad_node_pool.test"] + if resourceState == nil { + return errors.New("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return errors.New("resource has no primary instance") + } + + if instanceState.ID != name { + return fmt.Errorf("expected resource ID to be %q, got %q", name, instanceState.ID) + } + + client := testProvider.Meta().(ProviderConfig).client + pool, _, err := client.NodePools().Info(name, nil) + if err != nil { + return fmt.Errorf("error fetching node pool %q: %v", name, err) + } + + if pool.Name != name { + return fmt.Errorf("expected name to be %q, got %q", name, pool.Name) + } + + expectedDescription := "Terraform test node pool" + if pool.Description != expectedDescription { + return fmt.Errorf("expected description to be %q, got %q", expectedDescription, pool.Description) + } + + expectedMeta := map[string]string{ + "test": "true", + } + if diff := cmp.Diff(pool.Meta, expectedMeta); diff != "" { + return fmt.Errorf("meta mismatch (-want +got):\n%s", diff) + } + + if pool.SchedulerConfiguration == nil { + return fmt.Errorf("expected node pool to have scheduler configuration") + } + schedConfig := pool.SchedulerConfiguration + + expectedSchedAlgo := api.SchedulerAlgorithmSpread + if schedConfig.SchedulerAlgorithm != expectedSchedAlgo { + return fmt.Errorf( + "expected scheduler algorithm to be %q, got %q", + expectedSchedAlgo, + schedConfig.SchedulerAlgorithm, + ) + } + + if schedConfig.MemoryOversubscriptionEnabled == nil || !*schedConfig.MemoryOversubscriptionEnabled { + return fmt.Errorf("expected memory oversubscription to be enabled") + } + + return nil + } +} + +func testResourceNodePoolConfig_updated(name string) string { + return fmt.Sprintf(` +resource "nomad_node_pool" "test" { + name = "%s" + description = "Updated Terraform test node pool" + + scheduler_config { + scheduler_algorithm = "spread" + } +} +`, name) +} + +func testResourceNodePoolCheck_updated(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["nomad_node_pool.test"] + if resourceState == nil { + return errors.New("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return errors.New("resource has no primary instance") + } + + if instanceState.ID != name { + return fmt.Errorf("expected resource ID to be %q, got %q", name, instanceState.ID) + } + + client := testProvider.Meta().(ProviderConfig).client + pool, _, err := client.NodePools().Info(name, nil) + if err != nil { + return fmt.Errorf("error fetching node pool %q: %v", name, err) + } + + if pool.Name != name { + return fmt.Errorf("expected name to be %q, got %q", name, pool.Name) + } + + expectedDescription := "Updated Terraform test node pool" + if pool.Description != expectedDescription { + return fmt.Errorf("expected description to be %q, got %q", expectedDescription, pool.Description) + } + + if len(pool.Meta) != 0 { + return fmt.Errorf("expected meta to be empty") + } + + if pool.SchedulerConfiguration == nil { + return fmt.Errorf("expected node pool to have scheduler configuration") + } + schedConfig := pool.SchedulerConfiguration + + expectedSchedAlgo := api.SchedulerAlgorithmSpread + if schedConfig.SchedulerAlgorithm != expectedSchedAlgo { + return fmt.Errorf( + "expected scheduler algorithm to be %q, got %q", + expectedSchedAlgo, + schedConfig.SchedulerAlgorithm, + ) + } + + if schedConfig.MemoryOversubscriptionEnabled != nil { + return fmt.Errorf("expected memory oversubscription to not be set, got %v", + *schedConfig.MemoryOversubscriptionEnabled) + } + + return nil + } +} + +func testResourceNodePool_checkDestroy(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testProvider.Meta().(ProviderConfig).client + pool, _, err := client.NodePools().Info(name, nil) + if err != nil && strings.Contains(err.Error(), "404") || pool == nil { + return nil + } + return fmt.Errorf("node pool %q not deleted", name) + } +} + +func testResourceNodePool_delete(t *testing.T, name string) func() { + return func() { + client := testProvider.Meta().(ProviderConfig).client + _, err := client.NodePools().Delete(name, nil) + if err != nil { + t.Fatalf("error deleting node pool %q: %v", name, err) + } + } +} diff --git a/website/docs/d/node_pool.html.markdown b/website/docs/d/node_pool.html.markdown new file mode 100644 index 00000000..603c4892 --- /dev/null +++ b/website/docs/d/node_pool.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "nomad" +page_title: "Nomad: nomad_node_pool" +sidebar_current: "docs-nomad-datasource-node-pool" +description: |- + Get information about a node pool in Nomad. +--- + +# nomad_node_pool + +Get information about a node pool in Nomad. + +## Example Usage + +```hcl +data "nomad_node_pool" "dev" { + name = "dev" +} +``` + +## Argument Reference + +- `name` `(string)` - The name of the node pool to fetch. + +## Attribute Reference + +The following attributes are exported: + +- `description` `(string)` - The description of the node pool. +- `meta` `(map[string]string)` - Arbitrary KV metadata associated with the + node pool. +- `scheduler_config` `(block)` - Scheduler configuration for the node pool. + - `scheduler_algorithm` `(string)` - The scheduler algorithm used in the node + pool. If empty or not defined the global cluster configuration is used. + - `memory_oversubscription` `(string)` - Whether or not memory + oversubscription is enabled in the node pool. If empty or not defined the + global cluster configuration is used. + + -> This option differs from Nomad, where it's represented as a boolean, to + allow distinguishing between memory oversubscription being disabled in the + node pool and this property not being set. diff --git a/website/docs/d/node_pools.html.markdown b/website/docs/d/node_pools.html.markdown new file mode 100644 index 00000000..01667773 --- /dev/null +++ b/website/docs/d/node_pools.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "nomad" +page_title: "Nomad: nomad_node_pools" +sidebar_current: "docs-nomad-datasource-node-pools" +description: |- + Retrieve a list of node pools available in Nomad. +--- + +# nomad_node_pools + +Retrieve a list of node pools available in Nomad. + +## Example Usage + +```hcl +data "nomad_node_pools" "prod" { + filter = "Meta.env == \"prod\"" +} +``` + +## Argument Reference + +The following arguments are supported: + +- `prefix` `(string)` - Specifies a string to filter node pools based on a name + prefix. +- `filter` `(string)` - Specifies the [expression][nomad_api_filter] used to + filter the results. + +## Attribute Reference + +The following attributes are exported: + +- `node_pools` `(list of node pools)` - A list of node pools matching the + search criteria. + - `name` `(string)` - The name of the node pool. + - `description` `(string)` - The description of the node pool. + - `meta` `(map[string]string)` - Arbitrary KV metadata associated with the + node pool. + - `scheduler_config` `(block)` - Scheduler configuration for the node pool. + - `scheduler_algorithm` `(string)` - The scheduler algorithm used in the node + pool. If empty or not defined the global cluster configuration is used. + - `memory_oversubscription` `(string)` - Whether or not memory + oversubscription is enabled in the node pool. If empty or not defined the + global cluster configuration is used. + + -> This option differs from Nomad, where it's represented as a boolean, to + allow distinguishing between memory oversubscription being disabled in the + node pool and this property not being set. + +[nomad_api_filter]: https://developer.hashicorp.com/nomad/api-docs/v1.6.x#filtering diff --git a/website/docs/r/node_pool.html.markdown b/website/docs/r/node_pool.html.markdown new file mode 100644 index 00000000..ee5a0645 --- /dev/null +++ b/website/docs/r/node_pool.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "nomad" +page_title: "Nomad: nomad_node_pool" +sidebar_current: "docs-nomad-resource-node-pool" +description: |- + Provisions a node pool within a Nomad cluster. +--- + +# nomad_node_pool + +Provisions a node pool within a Nomad cluster. + +## Example Usage + +Registering a node pool: + +```hcl +resource "nomad_node_pool" "dev" { + name = "dev" + description = "Nodes for the development environment." + + meta = { + department = "Engineering" + env = "dev" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +- `name` `(string)` - The name of the node pool. +- `description` `(string)` - The description of the node pool. +- `meta` `(map[string]string)` - Arbitrary KV metadata associated with the + node pool. +- `scheduler_config` `(block)` - Scheduler configuration for the node pool. + - `scheduler_algorithm` `(string)` - The scheduler algorithm used in the node + pool. Possible values are `binpack` or `spread`. If not defined the global + cluster configuration is used. + - `memory_oversubscription` `(string)` - Whether or not memory + oversubscription is enabled in the node pool. Possible values are + `"enabled"` or `"disabled"`. If not defined the global cluster + configuration is used. + + -> This option differs from Nomad, where it's represented as a boolean, to + allow distinguishing between memory oversubscription being disabled in the + node pool and this property not being set. diff --git a/website/nomad.erb b/website/nomad.erb index 99a6070e..beef7b3c 100644 --- a/website/nomad.erb +++ b/website/nomad.erb @@ -35,14 +35,20 @@ nomad_job > - nomad_job_parser - + nomad_job_parser + > nomad_namespace > nomad_namespaces + > + nomad_node_pool + + > + nomad_node_pools + > nomad_plugin @@ -86,6 +92,9 @@ > nomad_namespace + > + nomad_node_pool + > nomad_quota_specification