mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
Most Nomad upsert RPCs accept a single object with the notable exception of CSI. But in CSI we don't actually expose this to users except through the Go API. It deeply complicates how we present errors to users, especially once Sentinel policy enforcement enters the mix. Refactor the `HostVolume.Create` and `HostVolume.Register` RPCs to take a single volume instead of a slice of volumes. Add a stub function for Enterprise policy enforcement. This requires splitting out placement from the `createVolume` function so that we can ensure we've completed placement before trying to enforce policy. Ref: https://github.com/hashicorp/nomad/pull/24479
364 lines
8.8 KiB
Go
364 lines
8.8 KiB
Go
// 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 bool) int {
|
|
|
|
vol, err := decodeHostVolume(ast)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error decoding the volume definition: %s", err))
|
|
return 1
|
|
}
|
|
|
|
req := &api.HostVolumeCreateRequest{
|
|
Volume: vol,
|
|
}
|
|
vol, _, err = client.HostVolumes().Create(req, nil)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error creating volume: %s", err))
|
|
return 1
|
|
}
|
|
|
|
var volID string
|
|
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 = vol.ModifyIndex
|
|
}
|
|
|
|
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.HostVolumeStateDeleted:
|
|
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_max")
|
|
delete(m, "capacity_min")
|
|
delete(m, "type")
|
|
|
|
// Decode the rest
|
|
err = mapstructure.WeakDecode(m, vol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|