fc313b7f8f
* Add UnexpectedResultError to nomad/api This allows users to perform additional status-based behavior by rehydrating the error using `errors.As` inside of consumers.
457 lines
13 KiB
Go
457 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// ErrVariableNotFound was used as the content of an error string.
|
|
//
|
|
// Deprecated: use ErrVariablePathNotFound instead.
|
|
ErrVariableNotFound = "variable not found"
|
|
)
|
|
|
|
var (
|
|
// ErrVariablePathNotFound is returned when trying to read a variable that
|
|
// does not exist.
|
|
ErrVariablePathNotFound = errors.New("variable not found")
|
|
)
|
|
|
|
// Variables is used to access variables.
|
|
type Variables struct {
|
|
client *Client
|
|
}
|
|
|
|
// Variables returns a new handle on the variables.
|
|
func (c *Client) Variables() *Variables {
|
|
return &Variables{client: c}
|
|
}
|
|
|
|
// Create is used to create a variable.
|
|
func (vars *Variables) Create(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
|
|
v.Path = cleanPathString(v.Path)
|
|
var out Variable
|
|
wm, err := vars.client.put("/v1/var/"+v.Path, v, &out, qo)
|
|
if err != nil {
|
|
return nil, wm, err
|
|
}
|
|
return &out, wm, nil
|
|
}
|
|
|
|
// CheckedCreate is used to create a variable if it doesn't exist
|
|
// already. If it does, it will return a ErrCASConflict that can be unwrapped
|
|
// for more details.
|
|
func (vars *Variables) CheckedCreate(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
|
|
v.Path = cleanPathString(v.Path)
|
|
var out Variable
|
|
wm, err := vars.writeChecked("/v1/var/"+v.Path+"?cas=0", v, &out, qo)
|
|
if err != nil {
|
|
return nil, wm, err
|
|
}
|
|
return &out, wm, nil
|
|
}
|
|
|
|
// Read is used to query a single variable by path. This will error
|
|
// if the variable is not found.
|
|
func (vars *Variables) Read(path string, qo *QueryOptions) (*Variable, *QueryMeta, error) {
|
|
path = cleanPathString(path)
|
|
var v = new(Variable)
|
|
qm, err := vars.readInternal("/v1/var/"+path, &v, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if v == nil {
|
|
return nil, qm, ErrVariablePathNotFound
|
|
}
|
|
return v, qm, nil
|
|
}
|
|
|
|
// Peek is used to query a single variable by path, but does not error
|
|
// when the variable is not found
|
|
func (vars *Variables) Peek(path string, qo *QueryOptions) (*Variable, *QueryMeta, error) {
|
|
path = cleanPathString(path)
|
|
var v = new(Variable)
|
|
qm, err := vars.readInternal("/v1/var/"+path, &v, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return v, qm, nil
|
|
}
|
|
|
|
// Update is used to update a variable.
|
|
func (vars *Variables) Update(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
|
|
v.Path = cleanPathString(v.Path)
|
|
var out Variable
|
|
|
|
wm, err := vars.client.put("/v1/var/"+v.Path, v, &out, qo)
|
|
if err != nil {
|
|
return nil, wm, err
|
|
}
|
|
return &out, wm, nil
|
|
}
|
|
|
|
// CheckedUpdate is used to updated a variable if the modify index
|
|
// matches the one on the server. If it does not, it will return an
|
|
// ErrCASConflict that can be unwrapped for more details.
|
|
func (vars *Variables) CheckedUpdate(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
|
|
v.Path = cleanPathString(v.Path)
|
|
var out Variable
|
|
|
|
wm, err := vars.writeChecked("/v1/var/"+v.Path+"?cas="+fmt.Sprint(v.ModifyIndex), v, &out, qo)
|
|
if err != nil {
|
|
return nil, wm, err
|
|
}
|
|
return &out, wm, nil
|
|
}
|
|
|
|
// Delete is used to delete a variable
|
|
func (vars *Variables) Delete(path string, qo *WriteOptions) (*WriteMeta, error) {
|
|
path = cleanPathString(path)
|
|
wm, err := vars.deleteInternal(path, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// CheckedDelete is used to conditionally delete a variable. If the
|
|
// existing variable does not match the provided checkIndex, it will return an
|
|
// ErrCASConflict that can be unwrapped for more details.
|
|
func (vars *Variables) CheckedDelete(path string, checkIndex uint64, qo *WriteOptions) (*WriteMeta, error) {
|
|
path = cleanPathString(path)
|
|
wm, err := vars.deleteChecked(path, checkIndex, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// List is used to dump all of the variables, can be used to pass prefix
|
|
// via QueryOptions rather than as a parameter
|
|
func (vars *Variables) List(qo *QueryOptions) ([]*VariableMetadata, *QueryMeta, error) {
|
|
var resp []*VariableMetadata
|
|
qm, err := vars.client.query("/v1/vars", &resp, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return resp, qm, nil
|
|
}
|
|
|
|
// PrefixList is used to do a prefix List search over variables.
|
|
func (vars *Variables) PrefixList(prefix string, qo *QueryOptions) ([]*VariableMetadata, *QueryMeta, error) {
|
|
if qo == nil {
|
|
qo = &QueryOptions{Prefix: prefix}
|
|
} else {
|
|
qo.Prefix = prefix
|
|
}
|
|
return vars.List(qo)
|
|
}
|
|
|
|
// GetItems returns the inner Items collection from a variable at a given path.
|
|
//
|
|
// Deprecated: Use GetVariableItems instead.
|
|
func (vars *Variables) GetItems(path string, qo *QueryOptions) (*VariableItems, *QueryMeta, error) {
|
|
vi, qm, err := vars.GetVariableItems(path, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return &vi, qm, nil
|
|
}
|
|
|
|
// GetVariableItems returns the inner Items collection from a variable at a given path.
|
|
func (vars *Variables) GetVariableItems(path string, qo *QueryOptions) (VariableItems, *QueryMeta, error) {
|
|
path = cleanPathString(path)
|
|
v := new(Variable)
|
|
|
|
qm, err := vars.readInternal("/v1/var/"+path, &v, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// note: readInternal will in fact turn our v into a nil if not found
|
|
if v == nil {
|
|
return nil, nil, ErrVariablePathNotFound
|
|
}
|
|
|
|
return v.Items, qm, nil
|
|
}
|
|
|
|
// readInternal exists because the API's higher-level read method requires
|
|
// the status code to be 200 (OK). For Peek(), we do not consider 403 (Permission
|
|
// Denied or 404 (Not Found) an error, this function just returns a nil in those
|
|
// cases.
|
|
func (vars *Variables) readInternal(endpoint string, out **Variable, q *QueryOptions) (*QueryMeta, error) {
|
|
// todo(shoenig): seems like this could just return a *Variable instead of taking
|
|
// in a **Variable and modifying it?
|
|
|
|
r, err := vars.client.newRequest("GET", endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.setQueryOptions(q)
|
|
|
|
checkFn := requireStatusIn(http.StatusOK, http.StatusNotFound, http.StatusForbidden)
|
|
rtt, resp, err := checkFn(vars.client.doRequest(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
qm := &QueryMeta{}
|
|
_ = parseQueryMeta(resp, qm)
|
|
qm.RequestTime = rtt
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
*out = nil
|
|
_ = resp.Body.Close()
|
|
return qm, nil
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusForbidden {
|
|
*out = nil
|
|
_ = resp.Body.Close()
|
|
// On a 403, there is no QueryMeta to parse, but consul-template--the
|
|
// main consumer of the Peek() func that calls this method needs the
|
|
// value to be non-zero; so set them to a reasonable but artificial
|
|
// value. Index 1 doesn't say anything about the cluster, and there
|
|
// has to be a KnownLeader to get a 403.
|
|
qm.LastIndex = 1
|
|
qm.KnownLeader = true
|
|
return qm, nil
|
|
}
|
|
|
|
defer func() {
|
|
_ = resp.Body.Close()
|
|
}()
|
|
if err = decodeBody(resp, out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return qm, nil
|
|
}
|
|
|
|
// deleteInternal exists because the API's higher-level delete method requires
|
|
// the status code to be 200 (OK). The SV HTTP API returns a 204 (No Content)
|
|
// on success.
|
|
func (vars *Variables) deleteInternal(path string, q *WriteOptions) (*WriteMeta, error) {
|
|
r, err := vars.client.newRequest("DELETE", fmt.Sprintf("/v1/var/%s", path))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.setWriteOptions(q)
|
|
|
|
checkFn := requireStatusIn(http.StatusOK, http.StatusNoContent)
|
|
rtt, resp, err := checkFn(vars.client.doRequest(r))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wm := &WriteMeta{RequestTime: rtt}
|
|
_ = parseWriteMeta(resp, wm)
|
|
return wm, nil
|
|
}
|
|
|
|
// deleteChecked exists because the API's higher-level delete method requires
|
|
// the status code to be OK. The SV HTTP API returns a 204 (No Content) on
|
|
// success and a 409 (Conflict) on a CAS error.
|
|
func (vars *Variables) deleteChecked(path string, checkIndex uint64, q *WriteOptions) (*WriteMeta, error) {
|
|
r, err := vars.client.newRequest("DELETE", fmt.Sprintf("/v1/var/%s?cas=%v", path, checkIndex))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.setWriteOptions(q)
|
|
checkFn := requireStatusIn(http.StatusOK, http.StatusNoContent, http.StatusConflict)
|
|
rtt, resp, err := checkFn(vars.client.doRequest(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wm := &WriteMeta{RequestTime: rtt}
|
|
_ = parseWriteMeta(resp, wm)
|
|
|
|
// The only reason we should decode the response body is if
|
|
// it is a conflict response. Otherwise, there won't be one.
|
|
if resp.StatusCode == http.StatusConflict {
|
|
|
|
conflict := new(Variable)
|
|
if err = decodeBody(resp, &conflict); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, ErrCASConflict{
|
|
Conflict: conflict,
|
|
CheckIndex: checkIndex,
|
|
}
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// writeChecked exists because the API's higher-level write method requires
|
|
// the status code to be OK. The SV HTTP API returns a 200 (OK) on
|
|
// success and a 409 (Conflict) on a CAS error.
|
|
func (vars *Variables) writeChecked(endpoint string, in *Variable, out *Variable, q *WriteOptions) (*WriteMeta, error) {
|
|
r, err := vars.client.newRequest("PUT", endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.setWriteOptions(q)
|
|
r.obj = in
|
|
|
|
checkFn := requireStatusIn(http.StatusOK, http.StatusNoContent, http.StatusConflict)
|
|
rtt, resp, err := checkFn(vars.client.doRequest(r))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = resp.Body.Close()
|
|
}()
|
|
|
|
wm := &WriteMeta{RequestTime: rtt}
|
|
_ = parseWriteMeta(resp, wm)
|
|
|
|
if resp.StatusCode == http.StatusConflict {
|
|
|
|
conflict := new(Variable)
|
|
if err = decodeBody(resp, &conflict); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, ErrCASConflict{
|
|
Conflict: conflict,
|
|
CheckIndex: in.ModifyIndex,
|
|
}
|
|
}
|
|
if out != nil {
|
|
if err = decodeBody(resp, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// Variable specifies the metadata and contents to be stored in the
|
|
// encrypted Nomad backend.
|
|
type Variable struct {
|
|
// Namespace is the Nomad namespace associated with the variable
|
|
Namespace string `hcl:"namespace"`
|
|
|
|
// Path is the path to the variable
|
|
Path string `hcl:"path"`
|
|
|
|
// CreateIndex tracks the index of creation time
|
|
CreateIndex uint64 `hcl:"create_index"`
|
|
|
|
// ModifyTime is the unix nano of the last modified time
|
|
ModifyIndex uint64 `hcl:"modify_index"`
|
|
|
|
// CreateTime is the unix nano of the creation time
|
|
CreateTime int64 `hcl:"create_time"`
|
|
|
|
// ModifyTime is the unix nano of the last modified time
|
|
ModifyTime int64 `hcl:"modify_time"`
|
|
|
|
// Items contains the k/v variable component
|
|
Items VariableItems `hcl:"items"`
|
|
}
|
|
|
|
// VariableMetadata specifies the metadata for a variable and
|
|
// is used as the list object
|
|
type VariableMetadata struct {
|
|
// Namespace is the Nomad namespace associated with the variable
|
|
Namespace string `hcl:"namespace"`
|
|
|
|
// Path is the path to the variable
|
|
Path string `hcl:"path"`
|
|
|
|
// CreateIndex tracks the index of creation time
|
|
CreateIndex uint64 `hcl:"create_index"`
|
|
|
|
// ModifyTime is the unix nano of the last modified time
|
|
ModifyIndex uint64 `hcl:"modify_index"`
|
|
|
|
// CreateTime is the unix nano of the creation time
|
|
CreateTime int64 `hcl:"create_time"`
|
|
|
|
// ModifyTime is the unix nano of the last modified time
|
|
ModifyTime int64 `hcl:"modify_time"`
|
|
}
|
|
|
|
// VariableItems are the key/value pairs of a Variable.
|
|
type VariableItems map[string]string
|
|
|
|
// NewVariable is a convenience method to more easily create a
|
|
// ready-to-use variable
|
|
func NewVariable(path string) *Variable {
|
|
return &Variable{
|
|
Path: path,
|
|
Items: make(VariableItems),
|
|
}
|
|
}
|
|
|
|
// Copy returns a new deep copy of this Variable
|
|
func (v *Variable) Copy() *Variable {
|
|
var out = *v
|
|
out.Items = make(VariableItems)
|
|
for key, value := range v.Items {
|
|
out.Items[key] = value
|
|
}
|
|
return &out
|
|
}
|
|
|
|
// Metadata returns the VariableMetadata component of
|
|
// a Variable. This can be useful for comparing against
|
|
// a List result.
|
|
func (v *Variable) Metadata() *VariableMetadata {
|
|
return &VariableMetadata{
|
|
Namespace: v.Namespace,
|
|
Path: v.Path,
|
|
CreateIndex: v.CreateIndex,
|
|
ModifyIndex: v.ModifyIndex,
|
|
CreateTime: v.CreateTime,
|
|
ModifyTime: v.ModifyTime,
|
|
}
|
|
}
|
|
|
|
// IsZeroValue can be used to test if a Variable has been changed
|
|
// from the default values it gets at creation
|
|
func (v *Variable) IsZeroValue() bool {
|
|
return *v.Metadata() == VariableMetadata{} && v.Items == nil
|
|
}
|
|
|
|
// cleanPathString removes leading and trailing slashes since they
|
|
// would trigger go's path cleaning/redirection behavior in the
|
|
// standard HTTP router
|
|
func cleanPathString(path string) string {
|
|
return strings.Trim(path, " /")
|
|
}
|
|
|
|
// AsJSON returns the Variable as a JSON-formatted string
|
|
func (v *Variable) AsJSON() string {
|
|
var b []byte
|
|
b, _ = json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// AsPrettyJSON returns the Variable as a JSON-formatted string with
|
|
// indentation
|
|
func (v *Variable) AsPrettyJSON() string {
|
|
var b []byte
|
|
b, _ = json.MarshalIndent(v, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
type ErrCASConflict struct {
|
|
CheckIndex uint64
|
|
Conflict *Variable
|
|
}
|
|
|
|
func (e ErrCASConflict) Error() string {
|
|
return fmt.Sprintf("cas conflict: expected ModifyIndex %v; found %v", e.CheckIndex, e.Conflict.ModifyIndex)
|
|
}
|