// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package command import ( "os" "strings" "testing" "github.com/hashicorp/cli" "github.com/hashicorp/hcl" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/command/agent" "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 { mode = "0700" } ` 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, "-detach", 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) // Verify we can update the volume without changes args = []string{"-address", url, "-detach", "-id", got.ID, file.Name()} code = cmd.Run(args) must.Eq(t, 0, code, must.Sprintf("got error: %s", ui.ErrorWriter.String())) list, _, err := client.HostVolumes().List(nil, &api.QueryOptions{Namespace: "prod"}) must.Len(t, 1, list, must.Sprintf("new volume should not be created on update")) } 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) }) } }