diff --git a/CHANGELOG.md b/CHANGELOG.md index c31e123b..bbae66ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,11 @@ IMPROVEMENTS: * **Target Nomad 1.6.0*: updated the nomad client to support Nomad API and jobspec version 1.6.0 ([#345](https://github.com/hashicorp/terraform-provider-nomad/pull/345)) * provider: add `skip_verify` configuration to skip TLS verification ([#319](https://github.com/hashicorp/terraform-provider-nomad/pull/319)) * provider: update Go to 1.20.5 ([#334](https://github.com/hashicorp/terraform-provider-nomad/pull/334)) +* data source/nomad_namespace: add `node_pool_config` attribute ([#355](https://github.com/hashicorp/terraform-provider-nomad/pull/355)) * resource/nomad_acl_policy: add support for `job_acl` ([#314](https://github.com/hashicorp/terraform-provider-nomad/pull/314)) * resource/nomad_csi_volume and resource/nomad_csi_volume_registration: add support to import existing volumes. ([#359](https://github.com/hashicorp/terraform-provider-nomad/pull/359)] * resource/nomad_job: add support to import existing jobs. ([#359](https://github.com/hashicorp/terraform-provider-nomad/pull/359)] +* resource/nomad_namespace: add `node_pool_config` attribute ([#355](https://github.com/hashicorp/terraform-provider-nomad/pull/355)) * resource/nomad_volume and resource/nomad_external_volume: add timeouts for volume creation, registration, deregistration, and deletion ([#346](https://github.com/hashicorp/terraform-provider-nomad/pull/346)) BUG FIXES: diff --git a/nomad/data_source_namespace.go b/nomad/data_source_namespace.go index 278ffc45..c92a733f 100644 --- a/nomad/data_source_namespace.go +++ b/nomad/data_source_namespace.go @@ -38,6 +38,32 @@ func dataSourceNamespace() *schema.Resource { Computed: true, Elem: resourceNamespaceCapabilities(), }, + "node_pool_config": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default": { + Computed: true, + Type: schema.TypeString, + }, + "allowed": { + Computed: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "denied": { + Computed: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, }, } } @@ -63,6 +89,9 @@ func namespaceDataSourceRead(d *schema.ResourceData, meta interface{}) error { if err = d.Set("capabilities", flattenNamespaceCapabilities(ns.Capabilities)); err != nil { return fmt.Errorf("Failed to set 'capabilities': %v", err) } + if err = d.Set("node_pool_config", flattenNamespaceNodePoolConfig(ns.NodePoolConfiguration)); err != nil { + return fmt.Errorf("Failed to set 'node_pool_config': %v", err) + } d.SetId(client.Address() + "/namespace/" + name) return nil diff --git a/nomad/data_source_namespace_test.go b/nomad/data_source_namespace_test.go index cc537c6d..61148ef1 100644 --- a/nomad/data_source_namespace_test.go +++ b/nomad/data_source_namespace_test.go @@ -18,7 +18,7 @@ func TestDataSourceNamespace(t *testing.T) { resource.Test(t, resource.TestCase{ Providers: testProviders, - PreCheck: func() { testAccPreCheck(t); testCheckEnt(t) }, + PreCheck: func() { testAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: testDataSourceDefaultNamespaceConfig, @@ -49,6 +49,68 @@ func TestDataSourceNamespace(t *testing.T) { }) } +func TestDataSourceNamespace_nodePoolConfig(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"); testCheckEnt(t) }, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +resource "nomad_namespace" "test" { + name = "%s" + + node_pool_config { + default = "dev" + allowed = ["prod", "qa"] + } +} + +data "nomad_namespace" "test" { + name = nomad_namespace.test.name +} +`, name), + + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nomad_namespace.test", "name", name), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.#", "1"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.default", "dev"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.allowed.#", "2"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.allowed.0", "prod"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.allowed.1", "qa"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.denied.#", "0"), + ), + }, + { + Config: fmt.Sprintf(` +resource "nomad_namespace" "test" { + name = "%s" + + node_pool_config { + default = "dev" + denied = ["prod", "qa"] + } +} + +data "nomad_namespace" "test" { + name = nomad_namespace.test.name +} +`, name), + + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nomad_namespace.test", "name", name), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.#", "1"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.default", "dev"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.denied.#", "2"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.denied.0", "prod"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.denied.1", "qa"), + resource.TestCheckResourceAttr("data.nomad_namespace.test", "node_pool_config.0.allowed.#", "0"), + ), + }, + }, + }) +} + const testDataSourceDefaultNamespaceConfig = ` data "nomad_namespace" "test" { name = "default" diff --git a/nomad/data_source_node_pool_test.go b/nomad/data_source_node_pool_test.go index 4f9f6742..43d1a6fc 100644 --- a/nomad/data_source_node_pool_test.go +++ b/nomad/data_source_node_pool_test.go @@ -16,7 +16,7 @@ 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") }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0") }, Steps: []resource.TestStep{ { Config: testDataSourceNodePoolConfig_builtIn, @@ -48,7 +48,7 @@ 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) }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0"); testCheckEnt(t) }, Steps: []resource.TestStep{ { Config: testDataSourceNodePoolConfig_schedConfig(name), diff --git a/nomad/resource_namespace.go b/nomad/resource_namespace.go index e6055359..a8825030 100644 --- a/nomad/resource_namespace.go +++ b/nomad/resource_namespace.go @@ -61,6 +61,17 @@ func resourceNamespace() *schema.Resource { Elem: resourceNamespaceCapabilities(), MaxItems: 1, }, + "node_pool_config": { + Description: "Node pool configuration", + Optional: true, + Type: schema.TypeList, + Elem: resourceNamespaceNodePoolConfig(), + MaxItems: 1, + + // Set as computed because in Nomad Enterprise the default node + // pool is set to `default` if not set. + Computed: true, + }, }, } } @@ -84,6 +95,37 @@ func resourceNamespaceCapabilities() *schema.Resource { } } +func resourceNamespaceNodePoolConfig() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default": { + Description: "The node pool to use when none are specified in the job.", + Optional: true, + Computed: true, + Type: schema.TypeString, + }, + "allowed": { + Description: "The list of node pools allowed to be used in this namespace. Cannot be used with denied.", + Optional: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + ConflictsWith: []string{"node_pool_config.0.denied"}, + }, + "denied": { + Description: "The list of node pools not allowed to be used in this namespace. Cannot be used with allowed.", + Optional: true, + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + ConflictsWith: []string{"node_pool_config.0.allowed"}, + }, + }, + } +} + func resourceNamespaceWrite(d *schema.ResourceData, meta interface{}) error { client := meta.(ProviderConfig).client @@ -97,12 +139,18 @@ func resourceNamespaceWrite(d *schema.ResourceData, meta interface{}) error { return err } + npConfig, err := expandNamespaceNodePoolConfig(d) + if err != nil { + return err + } + namespace := api.Namespace{ - Name: d.Get("name").(string), - Description: d.Get("description").(string), - Quota: d.Get("quota").(string), - Meta: m, - Capabilities: capabilities, + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Quota: d.Get("quota").(string), + Meta: m, + Capabilities: capabilities, + NodePoolConfiguration: npConfig, } log.Printf("[DEBUG] Upserting namespace %q", namespace.Name) @@ -173,6 +221,7 @@ func resourceNamespaceRead(d *schema.ResourceData, meta interface{}) error { d.Set("quota", namespace.Quota) d.Set("meta", namespace.Meta) d.Set("capabilities", flattenNamespaceCapabilities(namespace.Capabilities)) + d.Set("node_pool_config", flattenNamespaceNodePoolConfig(namespace.NodePoolConfiguration)) return nil } @@ -262,3 +311,81 @@ func expandNamespaceCapabilities(d *schema.ResourceData) (*api.NamespaceCapabili } return &res, nil } + +func expandNamespaceNodePoolConfig(d *schema.ResourceData) (*api.NamespaceNodePoolConfiguration, error) { + npConfigI := d.Get("node_pool_config").([]any) + if len(npConfigI) < 1 { + return nil, nil + } + + npConfig, ok := npConfigI[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("expected map[string]interface{} for namespace node pool configuration, got %T", npConfigI[0]) + } + + var res api.NamespaceNodePoolConfiguration + + if defaultI, ok := npConfig["default"]; ok { + defaultStr, ok := defaultI.(string) + if !ok { + return nil, fmt.Errorf("expected default to be a string, got %T", defaultI) + } + res.Default = defaultStr + + } + + if allowedI, ok := npConfig["allowed"]; ok { + allowedSet, ok := allowedI.(*schema.Set) + if !ok { + return nil, fmt.Errorf("expected allowed to be a *schema.Set, got %T", allowedI) + } + if allowedSet.Len() > 0 { + res.Allowed = make([]string, allowedSet.Len()) + for i, v := range allowedSet.List() { + res.Allowed[i] = v.(string) + } + } + } + + if deniedI, ok := npConfig["denied"]; ok { + deniedSet, ok := deniedI.(*schema.Set) + if !ok { + return nil, fmt.Errorf("expected denied to be a *schema.Set, got %T", deniedI) + } + if deniedSet.Len() > 0 { + res.Denied = make([]string, deniedSet.Len()) + for i, v := range deniedSet.List() { + res.Denied[i] = v.(string) + } + } + } + + return &res, nil +} +func flattenNamespaceNodePoolConfig(npConfig *api.NamespaceNodePoolConfiguration) []any { + if npConfig == nil { + return nil + } + + rawNpConfig := map[string]any{ + "default": npConfig.Default, + } + + if npConfig.Allowed != nil { + allowed := make([]any, len(npConfig.Allowed)) + for i, v := range npConfig.Allowed { + allowed[i] = v + } + rawNpConfig["allowed"] = allowed + } + + if npConfig.Denied != nil { + denied := make([]any, len(npConfig.Denied)) + for i, v := range npConfig.Denied { + denied[i] = v + } + rawNpConfig["denied"] = denied + } + + return []any{rawNpConfig} +} diff --git a/nomad/resource_namespace_test.go b/nomad/resource_namespace_test.go index 77a53670..96ef4bab 100644 --- a/nomad/resource_namespace_test.go +++ b/nomad/resource_namespace_test.go @@ -6,6 +6,8 @@ package nomad import ( "errors" "fmt" + "regexp" + "sort" "strings" "testing" @@ -129,6 +131,64 @@ func TestResourceNamespace_deleteDefault(t *testing.T) { }) } +func TestResourceNamespace_nodePoolConfig(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"); testCheckEnt(t) }, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +resource "nomad_namespace" "test" { + name = "%s" + + node_pool_config { + default = "dev" + allowed = ["prod"] + denied = ["qa"] + } +} +`, name), + ExpectError: regexp.MustCompile(".+allowed.+conflicts with.+denied"), + }, + { + Config: fmt.Sprintf(` +resource "nomad_namespace" "test" { + name = "%s" + + node_pool_config { + default = "dev" + allowed = ["prod", "qa"] + } +} +`, name), + Check: testResourceNamespace_nodePoolConfigCheck(name, &api.NamespaceNodePoolConfiguration{ + Default: "dev", + Allowed: []string{"prod", "qa"}, + Denied: nil, + }), + }, + { + Config: fmt.Sprintf(` +resource "nomad_namespace" "test" { + name = "%s" + + node_pool_config { + default = "dev" + denied = ["prod", "qa"] + } +} +`, name), + Check: testResourceNamespace_nodePoolConfigCheck(name, &api.NamespaceNodePoolConfiguration{ + Default: "dev", + Denied: []string{"prod", "qa"}, + Allowed: nil, + }), + }, + }, + }) +} + func testResourceNamespace_initialConfig(name string) string { return fmt.Sprintf(` resource "nomad_namespace" "test" { @@ -314,3 +374,56 @@ func testResourceNamespace_updateCheck(name string) resource.TestCheckFunc { return nil } } + +func testResourceNamespace_nodePoolConfigCheck(name string, expected *api.NamespaceNodePoolConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["nomad_namespace.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 ID to be %q, got %q", name, instanceState.ID) + } + + client := testProvider.Meta().(ProviderConfig).client + namespace, _, err := client.Namespaces().Info(name, nil) + if err != nil { + return fmt.Errorf("error reading back namespace %q: %s", name, err) + } + + if namespace.Name != name { + return fmt.Errorf("expected name to be %q, is %q in API", name, namespace.Name) + } + + npConfig := namespace.NodePoolConfiguration + if npConfig == nil { + return errors.New("expected node pool configuration to exist") + } + + sortNpConfigSets := cmp.Transformer( + "Sort", + func(npConfig *api.NamespaceNodePoolConfiguration) *api.NamespaceNodePoolConfiguration { + allowed := append([]string(nil), npConfig.Allowed...) + denied := append([]string(nil), npConfig.Denied...) + sort.Strings(allowed) + sort.Strings(denied) + return &api.NamespaceNodePoolConfiguration{ + Default: npConfig.Default, + Allowed: allowed, + Denied: denied, + } + }, + ) + if diff := cmp.Diff(npConfig, expected, sortNpConfigSets); diff != "" { + return fmt.Errorf("node pool configuration mismatch (-want +got):\n%s", diff) + } + + return nil + } +} diff --git a/nomad/resource_node_pool_test.go b/nomad/resource_node_pool_test.go index ae780b9d..417e139e 100644 --- a/nomad/resource_node_pool_test.go +++ b/nomad/resource_node_pool_test.go @@ -21,7 +21,7 @@ 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") }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0") }, Steps: []resource.TestStep{ { Config: testResourceNodePoolConfig_basic(name), @@ -41,7 +41,7 @@ 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) }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0"); testCheckEnt(t) }, Steps: []resource.TestStep{ { Config: testResourceNodePoolConfig_schedConfig(name), @@ -56,7 +56,7 @@ 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") }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0") }, Steps: []resource.TestStep{ { Config: testResourceNodePoolConfig_basic(name), @@ -78,7 +78,7 @@ 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) }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0"); testCheckEnt(t) }, Steps: []resource.TestStep{ { Config: testResourceNodePoolConfig_schedConfig(name), @@ -96,7 +96,7 @@ func TestResourceNodePool_update(t *testing.T) { func TestResourceNodePool_error(t *testing.T) { resource.Test(t, resource.TestCase{ Providers: testProviders, - PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0-beta.1") }, + PreCheck: func() { testAccPreCheck(t); testCheckMinVersion(t, "1.6.0") }, Steps: []resource.TestStep{ { Config: ` diff --git a/scripts/getnomad.sh b/scripts/getnomad.sh index e45c0e6a..645d10d9 100755 --- a/scripts/getnomad.sh +++ b/scripts/getnomad.sh @@ -5,7 +5,7 @@ set -e -NOMAD_VERSION='1.6.0-beta.1' +NOMAD_VERSION='1.6.0' if [[ -n "$NOMAD_LICENSE" || -n "$NOMAD_LICENSE_PATH" ]]; then NOMAD_VERSION=${NOMAD_VERSION}+ent fi diff --git a/website/docs/r/namespace.html.markdown b/website/docs/r/namespace.html.markdown index 90310d0a..dab3dfe8 100644 --- a/website/docs/r/namespace.html.markdown +++ b/website/docs/r/namespace.html.markdown @@ -63,8 +63,9 @@ The following arguments are supported: - `description` `(string: "")` - A description of the namespace. - `quota` `(string: "")` - A resource quota to attach to the namespace. - `meta` `(map[string]string: )` - Specifies arbitrary KV metadata to associate with the namespace. -- `capabilities` `(block: )` - A block of capabilities for the namespace. Can't +- `capabilities` `(block: )` - A block of capabilities for the namespace. Can't be repeated. See below for the structure of this block. +- `node_pool_config` `(block: )` - A block with node pool configuration for the namespace (Nomad Enterprise only). ### `capabilities` blocks @@ -73,4 +74,13 @@ The `capabilities` block describes the capabilities of the namespace. It support the following arguments: - `enabled_task_drivers` `([]string: )` - Task drivers enabled for the namespace. -- `disabled_task_drivers` `([]string: )` - Task drivers disabled for the namespace. \ No newline at end of file +- `disabled_task_drivers` `([]string: )` - Task drivers disabled for the namespace. + +### `node_pool_config` blocks + +The `node_pool_config` block describes the node pool configuration for the +namespace. + +- `default` `(string: )` - The default node pool for jobs that don't define one. +- `allowed` `([]string: )` - The list of node pools that are allowed to be used in this namespace. +- `denied` `([]string: )` - The list of node pools that are not allowed to be used in this namespace.