mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
When making a request to create a dynamic host volumes, users can pass a node pool and constraints instead of a specific node ID. This changeset implements a node scheduling logic by instantiating a filter by node pool and constraint checker borrowed from the scheduler package. Because host volumes with the same name can't land on the same host, we don't need to support `distinct_hosts`/`distinct_property`; this would be challenging anyways without building out a much larger node iteration mechanism to keep track of usage across multiple hosts. Ref: https://github.com/hashicorp/nomad/pull/24479
226 lines
4.5 KiB
Go
226 lines
4.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/nomad/api"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/command/agent"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/shoenig/test/must"
|
|
)
|
|
|
|
func TestHostVolumeCreateCommand_Run(t *testing.T) {
|
|
ci.Parallel(t)
|
|
srv, client, url := testServer(t, true, func(c *agent.Config) {
|
|
c.Client.Meta = map[string]string{"rack": "foo"}
|
|
})
|
|
t.Cleanup(srv.Shutdown)
|
|
|
|
waitForNodes(t, client)
|
|
|
|
_, err := client.Namespaces().Register(&api.Namespace{Name: "prod"}, nil)
|
|
must.NoError(t, err)
|
|
|
|
ui := cli.NewMockUi()
|
|
cmd := &VolumeCreateCommand{Meta: Meta{Ui: ui}}
|
|
|
|
hclTestFile := `
|
|
namespace = "prod"
|
|
name = "database"
|
|
type = "host"
|
|
plugin_id = "mkdir"
|
|
node_pool = "default"
|
|
|
|
capacity_min = "10GiB"
|
|
capacity_max = "20G"
|
|
|
|
constraint {
|
|
attribute = "${meta.rack}"
|
|
value = "foo"
|
|
}
|
|
|
|
capability {
|
|
access_mode = "single-node-writer"
|
|
attachment_mode = "file-system"
|
|
}
|
|
|
|
capability {
|
|
access_mode = "single-node-reader-only"
|
|
attachment_mode = "block-device"
|
|
}
|
|
|
|
parameters {
|
|
foo = "bar"
|
|
}
|
|
`
|
|
|
|
file, err := os.CreateTemp(t.TempDir(), "volume-test-*.hcl")
|
|
must.NoError(t, err)
|
|
_, err = file.WriteString(hclTestFile)
|
|
must.NoError(t, err)
|
|
|
|
args := []string{"-address", url, file.Name()}
|
|
|
|
code := cmd.Run(args)
|
|
must.Eq(t, 0, code, must.Sprintf("got error: %s", ui.ErrorWriter.String()))
|
|
|
|
out := ui.OutputWriter.String()
|
|
must.StrContains(t, out, "Created host volume")
|
|
parts := strings.Split(out, " ")
|
|
id := strings.TrimSpace(parts[len(parts)-1])
|
|
|
|
// Verify volume was created
|
|
got, _, err := client.HostVolumes().Get(id, &api.QueryOptions{Namespace: "prod"})
|
|
must.NoError(t, err)
|
|
must.NotNil(t, got)
|
|
}
|
|
|
|
func TestHostVolume_HCLDecode(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
hcl string
|
|
expected *api.HostVolume
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "full spec",
|
|
hcl: `
|
|
namespace = "prod"
|
|
name = "database"
|
|
type = "host"
|
|
plugin_id = "mkdir"
|
|
node_pool = "default"
|
|
|
|
capacity_min = "10GiB"
|
|
capacity_max = "20G"
|
|
|
|
constraint {
|
|
attribute = "${attr.kernel.name}"
|
|
value = "linux"
|
|
}
|
|
|
|
constraint {
|
|
attribute = "${meta.rack}"
|
|
value = "foo"
|
|
}
|
|
|
|
capability {
|
|
access_mode = "single-node-writer"
|
|
attachment_mode = "file-system"
|
|
}
|
|
|
|
capability {
|
|
access_mode = "single-node-reader-only"
|
|
attachment_mode = "block-device"
|
|
}
|
|
|
|
parameters {
|
|
foo = "bar"
|
|
}
|
|
`,
|
|
expected: &api.HostVolume{
|
|
Namespace: "prod",
|
|
Name: "database",
|
|
PluginID: "mkdir",
|
|
NodePool: "default",
|
|
Constraints: []*api.Constraint{{
|
|
LTarget: "${attr.kernel.name}",
|
|
RTarget: "linux",
|
|
Operand: "=",
|
|
}, {
|
|
LTarget: "${meta.rack}",
|
|
RTarget: "foo",
|
|
Operand: "=",
|
|
}},
|
|
RequestedCapacityMinBytes: 10737418240,
|
|
RequestedCapacityMaxBytes: 20000000000,
|
|
RequestedCapabilities: []*api.HostVolumeCapability{
|
|
{
|
|
AttachmentMode: api.HostVolumeAttachmentModeFilesystem,
|
|
AccessMode: api.HostVolumeAccessModeSingleNodeWriter,
|
|
},
|
|
{
|
|
AttachmentMode: api.HostVolumeAttachmentModeBlockDevice,
|
|
AccessMode: api.HostVolumeAccessModeSingleNodeReader,
|
|
},
|
|
},
|
|
Parameters: map[string]string{"foo": "bar"},
|
|
},
|
|
},
|
|
|
|
{
|
|
name: "mostly empty spec",
|
|
hcl: `
|
|
namespace = "prod"
|
|
name = "database"
|
|
type = "host"
|
|
plugin_id = "mkdir"
|
|
node_pool = "default"
|
|
`,
|
|
expected: &api.HostVolume{
|
|
Namespace: "prod",
|
|
Name: "database",
|
|
PluginID: "mkdir",
|
|
NodePool: "default",
|
|
},
|
|
},
|
|
|
|
{
|
|
name: "invalid capacity",
|
|
hcl: `
|
|
namespace = "prod"
|
|
name = "database"
|
|
type = "host"
|
|
plugin_id = "mkdir"
|
|
node_pool = "default"
|
|
|
|
capacity_min = "a"
|
|
`,
|
|
expected: nil,
|
|
errMsg: "invalid capacity_min: could not parse value as bytes: strconv.ParseFloat: parsing \"\": invalid syntax",
|
|
},
|
|
|
|
{
|
|
name: "invalid constraint",
|
|
hcl: `
|
|
namespace = "prod"
|
|
name = "database"
|
|
type = "host"
|
|
plugin_id = "mkdir"
|
|
node_pool = "default"
|
|
|
|
constraint {
|
|
distinct_hosts = "foo"
|
|
}
|
|
|
|
`,
|
|
expected: nil,
|
|
errMsg: "invalid constraint: distinct_hosts should be set to true or false; strconv.ParseBool: parsing \"foo\": invalid syntax",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ast, err := hcl.ParseString(tc.hcl)
|
|
must.NoError(t, err)
|
|
vol, err := decodeHostVolume(ast)
|
|
if tc.errMsg == "" {
|
|
must.NoError(t, err)
|
|
} else {
|
|
must.EqError(t, err, tc.errMsg)
|
|
}
|
|
must.Eq(t, tc.expected, vol)
|
|
})
|
|
}
|
|
|
|
}
|