// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package command import ( "context" "fmt" "strconv" "time" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/helper" "github.com/mitchellh/go-glint" "github.com/mitchellh/go-glint/components" "github.com/mitchellh/mapstructure" ) func (c *VolumeCreateCommand) hostVolumeCreate( client *api.Client, ast *ast.File, detach, verbose, override bool, volID string) int { vol, err := decodeHostVolume(ast) if err != nil { c.Ui.Error(fmt.Sprintf("Error decoding the volume definition: %s", err)) return 1 } if volID != "" { ns := c.namespace if vol.Namespace != "" { ns = vol.Namespace } stub, possible, err := getHostVolumeByPrefix(client, volID, ns) if err != nil { c.Ui.Error(fmt.Sprintf("Could not update existing volume: %s", err)) return 1 } if len(possible) > 0 { out, err := formatHostVolumes(possible, formatOpts{short: true}) if err != nil { c.Ui.Error(fmt.Sprintf("Error formatting: %s", err)) return 1 } c.Ui.Error(fmt.Sprintf("Prefix matched multiple volumes\n\n%s", out)) return 1 } vol.ID = stub.ID } req := &api.HostVolumeCreateRequest{ Volume: vol, PolicyOverride: override, } resp, meta, err := client.HostVolumes().Create(req, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error creating volume: %s", err)) return 1 } vol = resp.Volume if resp.Warnings != "" { c.Ui.Output( c.Colorize().Color( fmt.Sprintf("[bold][yellow]Volume Warnings:\n%s[reset]\n", resp.Warnings))) } var lastIndex uint64 if detach || vol.State == api.HostVolumeStateReady { c.Ui.Output(fmt.Sprintf( "Created host volume %s with ID %s", vol.Name, vol.ID)) return 0 } else { c.Ui.Output(fmt.Sprintf( "==> Created host volume %s with ID %s", vol.Name, vol.ID)) volID = vol.ID lastIndex = meta.LastIndex } if vol.Namespace != "" { client.SetNamespace(vol.Namespace) } err = c.monitorHostVolume(client, volID, lastIndex, verbose) if err != nil { c.Ui.Error(fmt.Sprintf("==> %s: %v", formatTime(time.Now()), err.Error())) return 1 } return 0 } func (c *VolumeCreateCommand) monitorHostVolume(client *api.Client, id string, lastIndex uint64, verbose bool) error { length := shortId if verbose { length = fullId } opts := formatOpts{ verbose: verbose, short: !verbose, length: length, } if isStdoutTerminal() { return c.ttyMonitor(client, id, lastIndex, opts) } else { return c.nottyMonitor(client, id, lastIndex, opts) } } func (c *VolumeCreateCommand) ttyMonitor(client *api.Client, id string, lastIndex uint64, opts formatOpts) error { gUi := glint.New() spinner := glint.Layout( components.Spinner(), glint.Text(fmt.Sprintf(" Monitoring volume %q in progress...", limit(id, opts.length))), ).Row().MarginLeft(2) refreshRate := 100 * time.Millisecond gUi.SetRefreshRate(refreshRate) gUi.Set(spinner) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go gUi.Render(ctx) qOpts := &api.QueryOptions{ AllowStale: true, WaitIndex: lastIndex, WaitTime: time.Second * 5, } var statusComponent *glint.LayoutComponent var endSpinner *glint.LayoutComponent DONE: for { vol, meta, err := client.HostVolumes().Get(id, qOpts) if err != nil { return err } str, err := formatHostVolume(vol, opts) if err != nil { // should never happen b/c we don't pass json/template via opts here return err } statusComponent = glint.Layout( glint.Text(""), glint.Text(formatTime(time.Now())), glint.Text(c.Colorize().Color(str)), ).MarginLeft(4) statusComponent = glint.Layout(statusComponent) gUi.Set(spinner, statusComponent) endSpinner = glint.Layout( components.Spinner(), glint.Text(fmt.Sprintf(" Host volume %q %s", limit(id, opts.length), vol.State)), ).Row().MarginLeft(2) switch vol.State { case api.HostVolumeStateReady: endSpinner = glint.Layout( glint.Text(fmt.Sprintf("✓ Host volume %q %s", limit(id, opts.length), vol.State)), ).Row().MarginLeft(2) break DONE case api.HostVolumeStateUnavailable: endSpinner = glint.Layout( glint.Text(fmt.Sprintf("! Host volume %q %s", limit(id, opts.length), vol.State)), ).Row().MarginLeft(2) break DONE default: qOpts.WaitIndex = meta.LastIndex continue } } // Render one final time with completion message gUi.Set(endSpinner, statusComponent, glint.Text("")) gUi.RenderFrame() return nil } func (c *VolumeCreateCommand) nottyMonitor(client *api.Client, id string, lastIndex uint64, opts formatOpts) error { c.Ui.Info(fmt.Sprintf("==> %s: Monitoring volume %q...", formatTime(time.Now()), limit(id, opts.length))) for { vol, _, err := client.HostVolumes().Get(id, &api.QueryOptions{ WaitIndex: lastIndex, WaitTime: time.Second * 5, }) if err != nil { return err } if vol.State == api.HostVolumeStateReady { c.Ui.Info(fmt.Sprintf("==> %s: Volume %q ready", formatTime(time.Now()), limit(vol.Name, opts.length))) return nil } } } func decodeHostVolume(input *ast.File) (*api.HostVolume, error) { var err error vol := &api.HostVolume{} list, ok := input.Node.(*ast.ObjectList) if !ok { return nil, fmt.Errorf("error parsing: root should be an object") } // Decode the full thing into a map[string]interface for ease var m map[string]any err = hcl.DecodeObject(&m, list) if err != nil { return nil, err } // Need to manually parse these fields/blocks delete(m, "capability") delete(m, "constraint") delete(m, "capacity") delete(m, "capacity_max") delete(m, "capacity_min") delete(m, "type") // Decode the rest err = mapstructure.WeakDecode(m, vol) if err != nil { return nil, err } capacity, err := parseCapacityBytes(list.Filter("capacity")) if err != nil { return nil, fmt.Errorf("invalid capacity: %v", err) } vol.CapacityBytes = capacity capacityMin, err := parseCapacityBytes(list.Filter("capacity_min")) if err != nil { return nil, fmt.Errorf("invalid capacity_min: %v", err) } vol.RequestedCapacityMinBytes = capacityMin capacityMax, err := parseCapacityBytes(list.Filter("capacity_max")) if err != nil { return nil, fmt.Errorf("invalid capacity_max: %v", err) } vol.RequestedCapacityMaxBytes = capacityMax if o := list.Filter("constraint"); len(o.Items) > 0 { if err := parseConstraints(&vol.Constraints, o); err != nil { return nil, fmt.Errorf("invalid constraint: %v", err) } } if o := list.Filter("capability"); len(o.Items) > 0 { if err := parseHostVolumeCapabilities(&vol.RequestedCapabilities, o); err != nil { return nil, fmt.Errorf("invalid capability: %v", err) } } return vol, nil } func parseHostVolumeCapabilities(result *[]*api.HostVolumeCapability, list *ast.ObjectList) error { for _, o := range list.Elem().Items { valid := []string{"access_mode", "attachment_mode"} if err := helper.CheckHCLKeys(o.Val, valid); err != nil { return err } ot, ok := o.Val.(*ast.ObjectType) if !ok { break } var m map[string]any if err := hcl.DecodeObject(&m, ot.List); err != nil { return err } var cap *api.HostVolumeCapability if err := mapstructure.WeakDecode(&m, &cap); err != nil { return err } *result = append(*result, cap) } return nil } func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error { for _, o := range list.Elem().Items { valid := []string{ "attribute", "distinct_hosts", "distinct_property", "operator", "regexp", "set_contains", "value", "version", "semver", } if err := helper.CheckHCLKeys(o.Val, valid); err != nil { return err } var m map[string]any if err := hcl.DecodeObject(&m, o.Val); err != nil { return err } m["LTarget"] = m["attribute"] m["RTarget"] = m["value"] m["Operand"] = m["operator"] // If "version" is provided, set the operand // to "version" and the value to the "RTarget" if constraint, ok := m[api.ConstraintVersion]; ok { m["Operand"] = api.ConstraintVersion m["RTarget"] = constraint } // If "semver" is provided, set the operand // to "semver" and the value to the "RTarget" if constraint, ok := m[api.ConstraintSemver]; ok { m["Operand"] = api.ConstraintSemver m["RTarget"] = constraint } // If "regexp" is provided, set the operand // to "regexp" and the value to the "RTarget" if constraint, ok := m[api.ConstraintRegex]; ok { m["Operand"] = api.ConstraintRegex m["RTarget"] = constraint } // If "set_contains" is provided, set the operand // to "set_contains" and the value to the "RTarget" if constraint, ok := m[api.ConstraintSetContains]; ok { m["Operand"] = api.ConstraintSetContains m["RTarget"] = constraint } if value, ok := m[api.ConstraintDistinctHosts]; ok { enabled, err := parseBool(value) if err != nil { return fmt.Errorf("distinct_hosts should be set to true or false; %v", err) } // If it is not enabled, skip the constraint. if !enabled { continue } m["Operand"] = api.ConstraintDistinctHosts m["RTarget"] = strconv.FormatBool(enabled) } if property, ok := m[api.ConstraintDistinctProperty]; ok { m["Operand"] = api.ConstraintDistinctProperty m["LTarget"] = property } // Build the constraint var c api.Constraint if err := mapstructure.WeakDecode(m, &c); err != nil { return err } if c.Operand == "" { c.Operand = "=" } *result = append(*result, &c) } return nil } // parseBool takes an interface value and tries to convert it to a boolean and // returns an error if the type can't be converted. func parseBool(value any) (bool, error) { var enabled bool var err error switch data := value.(type) { case string: enabled, err = strconv.ParseBool(data) case bool: enabled = data default: err = fmt.Errorf("%v couldn't be converted to boolean value", value) } return enabled, err }