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 }