github.com/hashicorp/nomad/api@v0.0.0-20240306165712-3193ac204f65/variables.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package api
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"strings"
    12  )
    13  
    14  const (
    15  	// ErrVariableNotFound was used as the content of an error string.
    16  	//
    17  	// Deprecated: use ErrVariablePathNotFound instead.
    18  	ErrVariableNotFound = "variable not found"
    19  )
    20  
    21  var (
    22  	// ErrVariablePathNotFound is returned when trying to read a variable that
    23  	// does not exist.
    24  	ErrVariablePathNotFound = errors.New("variable not found")
    25  )
    26  
    27  // Variables is used to access variables.
    28  type Variables struct {
    29  	client *Client
    30  }
    31  
    32  // Variables returns a new handle on the variables.
    33  func (c *Client) Variables() *Variables {
    34  	return &Variables{client: c}
    35  }
    36  
    37  // Create is used to create a variable.
    38  func (vars *Variables) Create(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
    39  	v.Path = cleanPathString(v.Path)
    40  	var out Variable
    41  	wm, err := vars.client.put("/v1/var/"+v.Path, v, &out, qo)
    42  	if err != nil {
    43  		return nil, wm, err
    44  	}
    45  	return &out, wm, nil
    46  }
    47  
    48  // CheckedCreate is used to create a variable if it doesn't exist
    49  // already. If it does, it will return a ErrCASConflict that can be unwrapped
    50  // for more details.
    51  func (vars *Variables) CheckedCreate(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
    52  	v.Path = cleanPathString(v.Path)
    53  	var out Variable
    54  	wm, err := vars.writeChecked("/v1/var/"+v.Path+"?cas=0", v, &out, qo)
    55  	if err != nil {
    56  		return nil, wm, err
    57  	}
    58  	return &out, wm, nil
    59  }
    60  
    61  // Read is used to query a single variable by path. This will error
    62  // if the variable is not found.
    63  func (vars *Variables) Read(path string, qo *QueryOptions) (*Variable, *QueryMeta, error) {
    64  	path = cleanPathString(path)
    65  	var v = new(Variable)
    66  	qm, err := vars.readInternal("/v1/var/"+path, &v, qo)
    67  	if err != nil {
    68  		return nil, nil, err
    69  	}
    70  	if v == nil {
    71  		return nil, qm, ErrVariablePathNotFound
    72  	}
    73  	return v, qm, nil
    74  }
    75  
    76  // Peek is used to query a single variable by path, but does not error
    77  // when the variable is not found
    78  func (vars *Variables) Peek(path string, qo *QueryOptions) (*Variable, *QueryMeta, error) {
    79  	path = cleanPathString(path)
    80  	var v = new(Variable)
    81  	qm, err := vars.readInternal("/v1/var/"+path, &v, qo)
    82  	if err != nil {
    83  		return nil, nil, err
    84  	}
    85  	return v, qm, nil
    86  }
    87  
    88  // Update is used to update a variable.
    89  func (vars *Variables) Update(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
    90  	v.Path = cleanPathString(v.Path)
    91  	var out Variable
    92  
    93  	wm, err := vars.client.put("/v1/var/"+v.Path, v, &out, qo)
    94  	if err != nil {
    95  		return nil, wm, err
    96  	}
    97  	return &out, wm, nil
    98  }
    99  
   100  // CheckedUpdate is used to updated a variable if the modify index
   101  // matches the one on the server.  If it does not, it will return an
   102  // ErrCASConflict that can be unwrapped for more details.
   103  func (vars *Variables) CheckedUpdate(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
   104  	v.Path = cleanPathString(v.Path)
   105  	var out Variable
   106  
   107  	wm, err := vars.writeChecked("/v1/var/"+v.Path+"?cas="+fmt.Sprint(v.ModifyIndex), v, &out, qo)
   108  	if err != nil {
   109  		return nil, wm, err
   110  	}
   111  	return &out, wm, nil
   112  }
   113  
   114  // Delete is used to delete a variable
   115  func (vars *Variables) Delete(path string, qo *WriteOptions) (*WriteMeta, error) {
   116  	path = cleanPathString(path)
   117  	wm, err := vars.deleteInternal(path, qo)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	return wm, nil
   122  }
   123  
   124  // CheckedDelete is used to conditionally delete a variable. If the
   125  // existing variable does not match the provided checkIndex, it will return an
   126  // ErrCASConflict that can be unwrapped for more details.
   127  func (vars *Variables) CheckedDelete(path string, checkIndex uint64, qo *WriteOptions) (*WriteMeta, error) {
   128  	path = cleanPathString(path)
   129  	wm, err := vars.deleteChecked(path, checkIndex, qo)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	return wm, nil
   134  }
   135  
   136  // List is used to dump all of the variables, can be used to pass prefix
   137  // via QueryOptions rather than as a parameter
   138  func (vars *Variables) List(qo *QueryOptions) ([]*VariableMetadata, *QueryMeta, error) {
   139  	var resp []*VariableMetadata
   140  	qm, err := vars.client.query("/v1/vars", &resp, qo)
   141  	if err != nil {
   142  		return nil, nil, err
   143  	}
   144  	return resp, qm, nil
   145  }
   146  
   147  // PrefixList is used to do a prefix List search over variables.
   148  func (vars *Variables) PrefixList(prefix string, qo *QueryOptions) ([]*VariableMetadata, *QueryMeta, error) {
   149  	if qo == nil {
   150  		qo = &QueryOptions{Prefix: prefix}
   151  	} else {
   152  		qo.Prefix = prefix
   153  	}
   154  	return vars.List(qo)
   155  }
   156  
   157  // GetItems returns the inner Items collection from a variable at a given path.
   158  //
   159  // Deprecated: Use GetVariableItems instead.
   160  func (vars *Variables) GetItems(path string, qo *QueryOptions) (*VariableItems, *QueryMeta, error) {
   161  	vi, qm, err := vars.GetVariableItems(path, qo)
   162  	if err != nil {
   163  		return nil, nil, err
   164  	}
   165  	return &vi, qm, nil
   166  }
   167  
   168  // GetVariableItems returns the inner Items collection from a variable at a given path.
   169  func (vars *Variables) GetVariableItems(path string, qo *QueryOptions) (VariableItems, *QueryMeta, error) {
   170  	path = cleanPathString(path)
   171  	v := new(Variable)
   172  
   173  	qm, err := vars.readInternal("/v1/var/"+path, &v, qo)
   174  	if err != nil {
   175  		return nil, nil, err
   176  	}
   177  
   178  	// note: readInternal will in fact turn our v into a nil if not found
   179  	if v == nil {
   180  		return nil, nil, ErrVariablePathNotFound
   181  	}
   182  
   183  	return v.Items, qm, nil
   184  }
   185  
   186  // RenewLock renews the lease for the lock on the given variable. It has to be called
   187  // before the lock's TTL expires or the lock will be automatically released after the
   188  // delay period.
   189  func (vars *Variables) RenewLock(v *Variable, qo *WriteOptions) (*VariableMetadata, *WriteMeta, error) {
   190  	v.Path = cleanPathString(v.Path)
   191  	var out VariableMetadata
   192  
   193  	wm, err := vars.client.put("/v1/var/"+v.Path+"?lock-renew", v, &out, qo)
   194  	if err != nil {
   195  		return nil, wm, err
   196  	}
   197  	return &out, wm, nil
   198  }
   199  
   200  // ReleaseLock removes the lock on the given variable.
   201  func (vars *Variables) ReleaseLock(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
   202  	return vars.lockOperation(v, qo, "lock-release")
   203  }
   204  
   205  // AcquireLock adds a lock on the given variable and starts a lease on it. In order
   206  // to make any update on the locked variable, the lock ID has to be included in the
   207  // request. In order to maintain ownership of the lock, the lease needs to be
   208  // periodically renewed before the lock's TTL expires.
   209  func (vars *Variables) AcquireLock(v *Variable, qo *WriteOptions) (*Variable, *WriteMeta, error) {
   210  	return vars.lockOperation(v, qo, "lock-acquire")
   211  }
   212  
   213  func (vars *Variables) lockOperation(v *Variable, qo *WriteOptions, operation string) (*Variable, *WriteMeta, error) {
   214  	v.Path = cleanPathString(v.Path)
   215  	var out Variable
   216  
   217  	wm, err := vars.client.put("/v1/var/"+v.Path+"?"+operation, v, &out, qo)
   218  	if err != nil {
   219  		return nil, wm, err
   220  	}
   221  	return &out, wm, nil
   222  }
   223  
   224  // readInternal exists because the API's higher-level read method requires
   225  // the status code to be 200 (OK). For Peek(), we do not consider 403 (Permission
   226  // Denied or 404 (Not Found) an error, this function just returns a nil in those
   227  // cases.
   228  func (vars *Variables) readInternal(endpoint string, out **Variable, q *QueryOptions) (*QueryMeta, error) {
   229  	// todo(shoenig): seems like this could just return a *Variable instead of taking
   230  	// in a **Variable and modifying it?
   231  
   232  	r, err := vars.client.newRequest("GET", endpoint)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	r.setQueryOptions(q)
   237  
   238  	checkFn := requireStatusIn(http.StatusOK, http.StatusNotFound, http.StatusForbidden) //nolint:bodyclose
   239  	rtt, resp, err := checkFn(vars.client.doRequest(r))                                  //nolint:bodyclose
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	qm := &QueryMeta{}
   245  	_ = parseQueryMeta(resp, qm)
   246  	qm.RequestTime = rtt
   247  
   248  	if resp.StatusCode == http.StatusNotFound {
   249  		*out = nil
   250  		_ = resp.Body.Close()
   251  		return qm, nil
   252  	}
   253  
   254  	if resp.StatusCode == http.StatusForbidden {
   255  		*out = nil
   256  		_ = resp.Body.Close()
   257  		// On a 403, there is no QueryMeta to parse, but consul-template--the
   258  		// main consumer of the Peek() func that calls this method needs the
   259  		// value to be non-zero; so set them to a reasonable but artificial
   260  		// value. Index 1 doesn't say anything about the cluster, and there
   261  		// has to be a KnownLeader to get a 403.
   262  		qm.LastIndex = 1
   263  		qm.KnownLeader = true
   264  		return qm, nil
   265  	}
   266  
   267  	defer func() {
   268  		_ = resp.Body.Close()
   269  	}()
   270  	if err = decodeBody(resp, out); err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	return qm, nil
   275  }
   276  
   277  // deleteInternal exists because the API's higher-level delete method requires
   278  // the status code to be 200 (OK). The SV HTTP API returns a 204 (No Content)
   279  // on success.
   280  func (vars *Variables) deleteInternal(path string, q *WriteOptions) (*WriteMeta, error) {
   281  	r, err := vars.client.newRequest("DELETE", fmt.Sprintf("/v1/var/%s", path))
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	r.setWriteOptions(q)
   286  
   287  	checkFn := requireStatusIn(http.StatusOK, http.StatusNoContent) //nolint:bodyclose
   288  	rtt, resp, err := checkFn(vars.client.doRequest(r))             //nolint:bodyclose
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	defer resp.Body.Close()
   293  
   294  	wm := &WriteMeta{RequestTime: rtt}
   295  	_ = parseWriteMeta(resp, wm)
   296  	return wm, nil
   297  }
   298  
   299  // deleteChecked exists because the API's higher-level delete method requires
   300  // the status code to be OK. The SV HTTP API returns a 204 (No Content) on
   301  // success and a 409 (Conflict) on a CAS error.
   302  func (vars *Variables) deleteChecked(path string, checkIndex uint64, q *WriteOptions) (*WriteMeta, error) {
   303  	r, err := vars.client.newRequest("DELETE", fmt.Sprintf("/v1/var/%s?cas=%v", path, checkIndex))
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	r.setWriteOptions(q)
   308  	checkFn := requireStatusIn(http.StatusOK, http.StatusNoContent, http.StatusConflict) //nolint:bodyclose
   309  	rtt, resp, err := checkFn(vars.client.doRequest(r))                                  //nolint:bodyclose
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	defer resp.Body.Close()
   314  
   315  	wm := &WriteMeta{RequestTime: rtt}
   316  	_ = parseWriteMeta(resp, wm)
   317  
   318  	// The only reason we should decode the response body is if
   319  	// it is a conflict response. Otherwise, there won't be one.
   320  	if resp.StatusCode == http.StatusConflict {
   321  
   322  		conflict := new(Variable)
   323  		if err = decodeBody(resp, &conflict); err != nil {
   324  			return nil, err
   325  		}
   326  		return nil, ErrCASConflict{
   327  			Conflict:   conflict,
   328  			CheckIndex: checkIndex,
   329  		}
   330  	}
   331  	return wm, nil
   332  }
   333  
   334  // writeChecked exists because the API's higher-level write method requires
   335  // the status code to be OK. The SV HTTP API returns a 200 (OK) on
   336  // success and a 409 (Conflict) on a CAS error.
   337  func (vars *Variables) writeChecked(endpoint string, in *Variable, out *Variable, q *WriteOptions) (*WriteMeta, error) {
   338  	r, err := vars.client.newRequest("PUT", endpoint)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  	r.setWriteOptions(q)
   343  	r.obj = in
   344  
   345  	checkFn := requireStatusIn(http.StatusOK, http.StatusNoContent, http.StatusConflict) //nolint:bodyclose
   346  	rtt, resp, err := checkFn(vars.client.doRequest(r))                                  //nolint:bodyclose
   347  
   348  	if err != nil {
   349  		return nil, err
   350  	}
   351  	defer func() {
   352  		_ = resp.Body.Close()
   353  	}()
   354  
   355  	wm := &WriteMeta{RequestTime: rtt}
   356  	_ = parseWriteMeta(resp, wm)
   357  
   358  	if resp.StatusCode == http.StatusConflict {
   359  
   360  		conflict := new(Variable)
   361  		if err = decodeBody(resp, &conflict); err != nil {
   362  			return nil, err
   363  		}
   364  		return nil, ErrCASConflict{
   365  			Conflict:   conflict,
   366  			CheckIndex: in.ModifyIndex,
   367  		}
   368  	}
   369  	if out != nil {
   370  		if err = decodeBody(resp, &out); err != nil {
   371  			return nil, err
   372  		}
   373  	}
   374  	return wm, nil
   375  }
   376  
   377  // Variable specifies the metadata and contents to be stored in the
   378  // encrypted Nomad backend.
   379  type Variable struct {
   380  	// Namespace is the Nomad namespace associated with the variable
   381  	Namespace string `hcl:"namespace"`
   382  
   383  	// Path is the path to the variable
   384  	Path string `hcl:"path"`
   385  
   386  	// CreateIndex tracks the index of creation time
   387  	CreateIndex uint64 `hcl:"create_index"`
   388  
   389  	// ModifyTime is the unix nano of the last modified time
   390  	ModifyIndex uint64 `hcl:"modify_index"`
   391  
   392  	// CreateTime is the unix nano of the creation time
   393  	CreateTime int64 `hcl:"create_time"`
   394  
   395  	// ModifyTime is the unix nano of the last modified time
   396  	ModifyTime int64 `hcl:"modify_time"`
   397  
   398  	// Items contains the k/v variable component
   399  	Items VariableItems `hcl:"items"`
   400  
   401  	// Lock holds the information about the variable lock if its being used.
   402  	Lock *VariableLock `hcl:",lock,optional" json:",omitempty"`
   403  }
   404  
   405  // VariableMetadata specifies the metadata for a variable and
   406  // is used as the list object
   407  type VariableMetadata struct {
   408  	// Namespace is the Nomad namespace associated with the variable
   409  	Namespace string `hcl:"namespace"`
   410  
   411  	// Path is the path to the variable
   412  	Path string `hcl:"path"`
   413  
   414  	// CreateIndex tracks the index of creation time
   415  	CreateIndex uint64 `hcl:"create_index"`
   416  
   417  	// ModifyTime is the unix nano of the last modified time
   418  	ModifyIndex uint64 `hcl:"modify_index"`
   419  
   420  	// CreateTime is the unix nano of the creation time
   421  	CreateTime int64 `hcl:"create_time"`
   422  
   423  	// ModifyTime is the unix nano of the last modified time
   424  	ModifyTime int64 `hcl:"modify_time"`
   425  
   426  	// Lock holds the information about the variable lock if its being used.
   427  	Lock *VariableLock `hcl:",lock,optional" json:",omitempty"`
   428  }
   429  
   430  type VariableLock struct {
   431  	// ID is generated by Nomad to provide a unique caller ID which can be used
   432  	// for renewals and unlocking.
   433  	ID string
   434  
   435  	// TTL describes the time-to-live of the current lock holder.
   436  	// This is a string version of a time.Duration like "2m".
   437  	TTL string
   438  
   439  	// LockDelay describes a grace period that exists after a lock is lost,
   440  	// before another client may acquire the lock. This helps protect against
   441  	// split-brains. This is a string version of a time.Duration like "2m".
   442  	LockDelay string
   443  }
   444  
   445  // VariableItems are the key/value pairs of a Variable.
   446  type VariableItems map[string]string
   447  
   448  // NewVariable is a convenience method to more easily create a
   449  // ready-to-use variable
   450  func NewVariable(path string) *Variable {
   451  	return &Variable{
   452  		Path:  path,
   453  		Items: make(VariableItems),
   454  	}
   455  }
   456  
   457  // Copy returns a new deep copy of this Variable
   458  func (v *Variable) Copy() *Variable {
   459  	var out = *v
   460  	out.Items = make(VariableItems)
   461  	for key, value := range v.Items {
   462  		out.Items[key] = value
   463  	}
   464  	return &out
   465  }
   466  
   467  // Metadata returns the VariableMetadata component of
   468  // a Variable. This can be useful for comparing against
   469  // a List result.
   470  func (v *Variable) Metadata() *VariableMetadata {
   471  	return &VariableMetadata{
   472  		Namespace:   v.Namespace,
   473  		Path:        v.Path,
   474  		CreateIndex: v.CreateIndex,
   475  		ModifyIndex: v.ModifyIndex,
   476  		CreateTime:  v.CreateTime,
   477  		ModifyTime:  v.ModifyTime,
   478  	}
   479  }
   480  
   481  // IsZeroValue can be used to test if a Variable has been changed
   482  // from the default values it gets at creation
   483  func (v *Variable) IsZeroValue() bool {
   484  	return *v.Metadata() == VariableMetadata{} && v.Items == nil
   485  }
   486  
   487  // cleanPathString removes leading and trailing slashes since they
   488  // would trigger go's path cleaning/redirection behavior in the
   489  // standard HTTP router
   490  func cleanPathString(path string) string {
   491  	return strings.Trim(path, " /")
   492  }
   493  
   494  // AsJSON returns the Variable as a JSON-formatted string
   495  func (v *Variable) AsJSON() string {
   496  	var b []byte
   497  	b, _ = json.Marshal(v)
   498  	return string(b)
   499  }
   500  
   501  // AsPrettyJSON returns the Variable as a JSON-formatted string with
   502  // indentation
   503  func (v *Variable) AsPrettyJSON() string {
   504  	var b []byte
   505  	b, _ = json.MarshalIndent(v, "", "  ")
   506  	return string(b)
   507  }
   508  
   509  // LockID returns the ID of the lock. In the event this is not held, or the
   510  // variable is not a lock, this string will be empty.
   511  func (v *Variable) LockID() string {
   512  	if v.Lock == nil {
   513  		return ""
   514  	}
   515  
   516  	return v.Lock.ID
   517  }
   518  
   519  type ErrCASConflict struct {
   520  	CheckIndex uint64
   521  	Conflict   *Variable
   522  }
   523  
   524  func (e ErrCASConflict) Error() string {
   525  	return fmt.Sprintf("cas conflict: expected ModifyIndex %v; found %v", e.CheckIndex, e.Conflict.ModifyIndex)
   526  }