448 lines
12 KiB
Go
448 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
ErrVariableNotFound = "secure variable not found"
|
|
ErrVariableMissingItems = "secure variable missing Items field"
|
|
)
|
|
|
|
// SecureVariables is used to access secure variables.
|
|
type SecureVariables struct {
|
|
client *Client
|
|
}
|
|
|
|
// SecureVariables returns a new handle on the secure variables.
|
|
func (c *Client) SecureVariables() *SecureVariables {
|
|
return &SecureVariables{client: c}
|
|
}
|
|
|
|
// Create is used to create a secure variable.
|
|
func (sv *SecureVariables) Create(v *SecureVariable, qo *WriteOptions) (*WriteMeta, error) {
|
|
|
|
v.Path = cleanPathString(v.Path)
|
|
wm, err := sv.client.write("/v1/var/"+v.Path, v, nil, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// CheckedCreate is used to create a secure variable if it doesn't exist
|
|
// already. If it does, it will return a ErrCASConflict that can be unwrapped
|
|
// for more details.
|
|
func (sv *SecureVariables) CheckedCreate(v *SecureVariable, qo *WriteOptions) (*WriteMeta, error) {
|
|
|
|
v.Path = cleanPathString(v.Path)
|
|
wm, err := sv.writeChecked("/v1/var/"+v.Path+"?cas=0", v, nil, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return wm, nil
|
|
}
|
|
|
|
// Read is used to query a single secure variable by path. This will error
|
|
// if the variable is not found.
|
|
func (sv *SecureVariables) Read(path string, qo *QueryOptions) (*SecureVariable, *QueryMeta, error) {
|
|
|
|
path = cleanPathString(path)
|
|
var svar = new(SecureVariable)
|
|
qm, err := sv.readInternal("/v1/var/"+path, &svar, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if svar == nil {
|
|
return nil, qm, errors.New(ErrVariableNotFound)
|
|
}
|
|
return svar, qm, nil
|
|
}
|
|
|
|
// Peek is used to query a single secure variable by path, but does not error
|
|
// when the variable is not found
|
|
func (sv *SecureVariables) Peek(path string, qo *QueryOptions) (*SecureVariable, *QueryMeta, error) {
|
|
|
|
path = cleanPathString(path)
|
|
var svar = new(SecureVariable)
|
|
qm, err := sv.readInternal("/v1/var/"+path, &svar, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return svar, qm, nil
|
|
}
|
|
|
|
// Update is used to update a secure variable.
|
|
func (sv *SecureVariables) Update(v *SecureVariable, qo *WriteOptions) (*WriteMeta, error) {
|
|
|
|
v.Path = cleanPathString(v.Path)
|
|
wm, err := sv.client.write("/v1/var/"+v.Path, v, nil, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// CheckedUpdate is used to updated a secure 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 (sv *SecureVariables) CheckedUpdate(v *SecureVariable, qo *WriteOptions) (*WriteMeta, error) {
|
|
|
|
v.Path = cleanPathString(v.Path)
|
|
wm, err := sv.writeChecked("/v1/var/"+v.Path+"?cas="+fmt.Sprint(v.ModifyIndex), v, nil, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return wm, nil
|
|
}
|
|
|
|
// Delete is used to delete a secure variable
|
|
func (sv *SecureVariables) Delete(path string, qo *WriteOptions) (*WriteMeta, error) {
|
|
|
|
path = cleanPathString(path)
|
|
wm, err := sv.deleteInternal(path, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// CheckedDelete is used to conditionally delete a secure variable. If the
|
|
// existing variable does not match the provided checkIndex, it will return an
|
|
// ErrCASConflict that can be unwrapped for more details.
|
|
func (sv *SecureVariables) CheckedDelete(path string, checkIndex uint64, qo *WriteOptions) (*WriteMeta, error) {
|
|
|
|
path = cleanPathString(path)
|
|
wm, err := sv.deleteChecked(path, checkIndex, qo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return wm, nil
|
|
}
|
|
|
|
// List is used to dump all of the secure variables, can be used to pass prefix
|
|
// via QueryOptions rather than as a parameter
|
|
func (sv *SecureVariables) List(qo *QueryOptions) ([]*SecureVariableMetadata, *QueryMeta, error) {
|
|
|
|
var resp []*SecureVariableMetadata
|
|
qm, err := sv.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 secure variables.
|
|
func (sv *SecureVariables) PrefixList(prefix string, qo *QueryOptions) ([]*SecureVariableMetadata, *QueryMeta, error) {
|
|
|
|
if qo == nil {
|
|
qo = &QueryOptions{Prefix: prefix}
|
|
} else {
|
|
qo.Prefix = prefix
|
|
}
|
|
|
|
return sv.List(qo)
|
|
}
|
|
|
|
// GetItems returns the inner Items collection from a secure variable at a
|
|
// given path
|
|
func (sv *SecureVariables) GetItems(path string, qo *QueryOptions) (*SecureVariableItems, *QueryMeta, error) {
|
|
|
|
path = cleanPathString(path)
|
|
svar := new(SecureVariable)
|
|
|
|
qm, err := sv.readInternal("/v1/var/"+path, &svar, qo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &svar.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 404
|
|
// (Not Found) an error.
|
|
func (sv *SecureVariables) readInternal(endpoint string, out **SecureVariable, q *QueryOptions) (*QueryMeta, error) {
|
|
|
|
r, err := sv.client.newRequest("GET", endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.setQueryOptions(q)
|
|
|
|
checkFn := requireStatusIn(http.StatusOK, http.StatusNotFound)
|
|
rtt, resp, err := checkFn(sv.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
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
if err := decodeBody(resp, out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return qm, nil
|
|
}
|
|
|
|
// readInternal 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 (sv *SecureVariables) deleteInternal(path string, q *WriteOptions) (*WriteMeta, error) {
|
|
|
|
r, err := sv.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(sv.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 (sv *SecureVariables) deleteChecked(path string, checkIndex uint64, q *WriteOptions) (*WriteMeta, error) {
|
|
|
|
r, err := sv.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(sv.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(SecureVariable)
|
|
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 (sv *SecureVariables) writeChecked(endpoint string, in *SecureVariable, out *SecureVariable, q *WriteOptions) (*WriteMeta, error) {
|
|
|
|
r, err := sv.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(sv.client.doRequest(r))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wm := &WriteMeta{RequestTime: rtt}
|
|
parseWriteMeta(resp, wm)
|
|
|
|
if resp.StatusCode == http.StatusConflict {
|
|
|
|
conflict := new(SecureVariable)
|
|
if err := decodeBody(resp, &conflict); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, ErrCASConflict{
|
|
Conflict: conflict,
|
|
CheckIndex: in.ModifyIndex,
|
|
}
|
|
}
|
|
return wm, nil
|
|
}
|
|
|
|
// SecureVariable specifies the metadata and contents to be stored in the
|
|
// encrypted Nomad backend.
|
|
type SecureVariable struct {
|
|
// Namespace is the Nomad namespace associated with the secure variable
|
|
Namespace string
|
|
// Path is the path to the secure variable
|
|
Path string
|
|
|
|
// Raft indexes to track creation and modification
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
|
|
// Times provided as a convenience for operators expressed time.UnixNanos
|
|
CreateTime int64
|
|
ModifyTime int64
|
|
|
|
Items SecureVariableItems
|
|
}
|
|
|
|
// SecureVariableMetadata specifies the metadata for a secure variable and
|
|
// is used as the list object
|
|
type SecureVariableMetadata struct {
|
|
// Namespace is the Nomad namespace associated with the secure variable
|
|
Namespace string
|
|
// Path is the path to the secure variable
|
|
Path string
|
|
|
|
// Raft indexes to track creation and modification
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
|
|
// Times provided as a convenience for operators expressed time.UnixNanos
|
|
CreateTime int64
|
|
ModifyTime int64
|
|
}
|
|
|
|
type SecureVariableItems map[string]string
|
|
|
|
// NewSecureVariable is a convenience method to more easily create a
|
|
// ready-to-use secure variable
|
|
func NewSecureVariable(path string) *SecureVariable {
|
|
|
|
return &SecureVariable{
|
|
Path: path,
|
|
Items: make(SecureVariableItems),
|
|
}
|
|
}
|
|
|
|
// Copy returns a new deep copy of this SecureVariable
|
|
func (sv1 *SecureVariable) Copy() *SecureVariable {
|
|
|
|
var out SecureVariable = *sv1
|
|
out.Items = make(SecureVariableItems)
|
|
for k, v := range sv1.Items {
|
|
out.Items[k] = v
|
|
}
|
|
return &out
|
|
}
|
|
|
|
// Metadata returns the SecureVariableMetadata component of
|
|
// a SecureVariable. This can be useful for comparing against
|
|
// a List result.
|
|
func (sv *SecureVariable) Metadata() *SecureVariableMetadata {
|
|
|
|
return &SecureVariableMetadata{
|
|
Namespace: sv.Namespace,
|
|
Path: sv.Path,
|
|
CreateIndex: sv.CreateIndex,
|
|
ModifyIndex: sv.ModifyIndex,
|
|
CreateTime: sv.CreateTime,
|
|
ModifyTime: sv.ModifyTime,
|
|
}
|
|
}
|
|
|
|
// IsZeroValue can be used to test if a SecureVariable has been changed
|
|
// from the default values it gets at creation
|
|
func (sv *SecureVariable) IsZeroValue() bool {
|
|
return *sv.Metadata() == SecureVariableMetadata{} && sv.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 SecureVariable as a JSON-formatted string
|
|
func (sv SecureVariable) AsJSON() string {
|
|
var b []byte
|
|
b, _ = json.Marshal(sv)
|
|
return string(b)
|
|
}
|
|
|
|
// AsPrettyJSON returns the SecureVariable as a JSON-formatted string with
|
|
// indentation
|
|
func (sv SecureVariable) AsPrettyJSON() string {
|
|
var b []byte
|
|
b, _ = json.MarshalIndent(sv, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
type ErrCASConflict struct {
|
|
CheckIndex uint64
|
|
Conflict *SecureVariable
|
|
}
|
|
|
|
func (e ErrCASConflict) Error() string {
|
|
return fmt.Sprintf("cas conflict: expected ModifyIndex %v; found %v", e.CheckIndex, e.Conflict.ModifyIndex)
|
|
}
|
|
|
|
// doRequestWrapper is a function that wraps the client's doRequest method
|
|
// and can be used to provide error and response handling
|
|
type doRequestWrapper = func(time.Duration, *http.Response, error) (time.Duration, *http.Response, error)
|
|
|
|
// requireStatusIn is a doRequestWrapper generator that takes expected HTTP
|
|
// response codes and validates that the received response code is among them
|
|
func requireStatusIn(statuses ...int) doRequestWrapper {
|
|
fn := func(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
|
|
statuses := statuses
|
|
if e != nil {
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
return d, nil, e
|
|
}
|
|
|
|
for _, status := range statuses {
|
|
if resp.StatusCode == status {
|
|
return d, resp, nil
|
|
}
|
|
}
|
|
|
|
return d, nil, generateUnexpectedResponseCodeError(resp)
|
|
}
|
|
return fn
|
|
}
|
|
|
|
// generateUnexpectedResponseCodeError creates a standardized error
|
|
// when the the API client's newRequest method receives an unexpected
|
|
// HTTP response code when accessing the secure variable's HTTP API
|
|
func generateUnexpectedResponseCodeError(resp *http.Response) error {
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, resp.Body)
|
|
resp.Body.Close()
|
|
return fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
|
|
}
|