github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/nomad/state/state_store_variables.go (about) 1 package state 2 3 import ( 4 "fmt" 5 "math" 6 7 "github.com/hashicorp/go-memdb" 8 9 "github.com/hashicorp/nomad/helper" 10 "github.com/hashicorp/nomad/nomad/structs" 11 ) 12 13 // Variables queries all the variables and is used only for 14 // snapshot/restore and key rotation 15 func (s *StateStore) Variables(ws memdb.WatchSet) (memdb.ResultIterator, error) { 16 txn := s.db.ReadTxn() 17 18 iter, err := txn.Get(TableVariables, indexID) 19 if err != nil { 20 return nil, err 21 } 22 23 ws.Add(iter.WatchCh()) 24 return iter, nil 25 } 26 27 // GetVariablesByNamespace returns an iterator that contains all 28 // variables belonging to the provided namespace. 29 func (s *StateStore) GetVariablesByNamespace( 30 ws memdb.WatchSet, namespace string) (memdb.ResultIterator, error) { 31 txn := s.db.ReadTxn() 32 return s.getVariablesByNamespaceImpl(txn, ws, namespace) 33 } 34 35 func (s *StateStore) getVariablesByNamespaceImpl( 36 txn *txn, ws memdb.WatchSet, namespace string) (memdb.ResultIterator, error) { 37 // Walk the entire table. 38 iter, err := txn.Get(TableVariables, indexID+"_prefix", namespace, "") 39 if err != nil { 40 return nil, fmt.Errorf("variable lookup failed: %v", err) 41 } 42 ws.Add(iter.WatchCh()) 43 44 return iter, nil 45 } 46 47 // GetVariablesByNamespaceAndPrefix returns an iterator that contains all 48 // variables belonging to the provided namespace that match the prefix. 49 func (s *StateStore) GetVariablesByNamespaceAndPrefix( 50 ws memdb.WatchSet, namespace, prefix string) (memdb.ResultIterator, error) { 51 txn := s.db.ReadTxn() 52 53 // Walk the entire table. 54 iter, err := txn.Get(TableVariables, indexID+"_prefix", namespace, prefix) 55 if err != nil { 56 return nil, fmt.Errorf("variable lookup failed: %v", err) 57 } 58 ws.Add(iter.WatchCh()) 59 60 return iter, nil 61 } 62 63 // GetVariablesByPrefix returns an iterator that contains all variables that 64 // match the prefix in any namespace. Namespace filtering is the responsibility 65 // of the caller. 66 func (s *StateStore) GetVariablesByPrefix( 67 ws memdb.WatchSet, prefix string) (memdb.ResultIterator, error) { 68 txn := s.db.ReadTxn() 69 70 // Walk the entire table. 71 iter, err := txn.Get(TableVariables, indexPath+"_prefix", prefix) 72 if err != nil { 73 return nil, fmt.Errorf("variable lookup failed: %v", err) 74 } 75 ws.Add(iter.WatchCh()) 76 77 return iter, nil 78 } 79 80 // GetVariablesByKeyID returns an iterator that contains all 81 // variables that were encrypted with a particular key 82 func (s *StateStore) GetVariablesByKeyID( 83 ws memdb.WatchSet, keyID string) (memdb.ResultIterator, error) { 84 txn := s.db.ReadTxn() 85 86 iter, err := txn.Get(TableVariables, indexKeyID, keyID) 87 if err != nil { 88 return nil, fmt.Errorf("variable lookup failed: %v", err) 89 } 90 ws.Add(iter.WatchCh()) 91 92 return iter, nil 93 } 94 95 // GetVariable returns a single variable at a given namespace and 96 // path. 97 func (s *StateStore) GetVariable( 98 ws memdb.WatchSet, namespace, path string) (*structs.VariableEncrypted, error) { 99 txn := s.db.ReadTxn() 100 101 // Try to fetch the variable. 102 watchCh, raw, err := txn.FirstWatch(TableVariables, indexID, namespace, path) 103 if err != nil { // error during fetch 104 return nil, fmt.Errorf("variable lookup failed: %v", err) 105 } 106 ws.Add(watchCh) 107 if raw == nil { // not found 108 return nil, nil 109 } 110 111 sv := raw.(*structs.VariableEncrypted) 112 return sv, nil 113 } 114 115 // VarSet is used to store a variable object. 116 func (s *StateStore) VarSet(idx uint64, sv *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 117 tx := s.db.WriteTxn(idx) 118 defer tx.Abort() 119 120 // Perform the actual set. 121 resp := s.varSetTxn(tx, idx, sv) 122 if resp.IsError() { 123 return resp 124 } 125 126 if err := tx.Commit(); err != nil { 127 return sv.ErrorResponse(idx, err) 128 } 129 return resp 130 } 131 132 // VarSetCAS is used to do a check-and-set operation on a 133 // variable. The ModifyIndex in the provided entry is used to determine if 134 // we should write the entry to the state store or not. 135 func (s *StateStore) VarSetCAS(idx uint64, sv *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 136 tx := s.db.WriteTxn(idx) 137 defer tx.Abort() 138 139 resp := s.varSetCASTxn(tx, idx, sv) 140 if resp.IsError() || resp.IsConflict() { 141 return resp 142 } 143 144 if err := tx.Commit(); err != nil { 145 return sv.ErrorResponse(idx, err) 146 } 147 return resp 148 } 149 150 // varSetCASTxn is the inner method used to do a CAS inside an existing 151 // transaction. 152 func (s *StateStore) varSetCASTxn(tx WriteTxn, idx uint64, req *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 153 sv := req.Var 154 raw, err := tx.First(TableVariables, indexID, sv.Namespace, sv.Path) 155 if err != nil { 156 return req.ErrorResponse(idx, fmt.Errorf("failed variable lookup: %s", err)) 157 } 158 svEx, ok := raw.(*structs.VariableEncrypted) 159 160 // ModifyIndex of 0 means that we are doing a set-if-not-exists. 161 if sv.ModifyIndex == 0 && raw != nil { 162 return req.ConflictResponse(idx, svEx) 163 } 164 165 // If the ModifyIndex is set but the variable doesn't exist, return a 166 // plausible zero value as the conflict 167 if sv.ModifyIndex != 0 && raw == nil { 168 zeroVal := &structs.VariableEncrypted{ 169 VariableMetadata: structs.VariableMetadata{ 170 Namespace: sv.Namespace, 171 Path: sv.Path, 172 }, 173 } 174 return req.ConflictResponse(idx, zeroVal) 175 } 176 177 // If the existing index does not match the provided CAS index arg, then we 178 // shouldn't update anything and can safely return early here. 179 if ok && sv.ModifyIndex != svEx.ModifyIndex { 180 return req.ConflictResponse(idx, svEx) 181 } 182 183 // If we made it this far, we should perform the set. 184 return s.varSetTxn(tx, idx, req) 185 } 186 187 // varSetTxn is used to insert or update a variable in the state 188 // store. It is the inner method used and handles only the actual storage. 189 func (s *StateStore) varSetTxn(tx WriteTxn, idx uint64, req *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 190 sv := req.Var 191 existingRaw, err := tx.First(TableVariables, indexID, sv.Namespace, sv.Path) 192 if err != nil { 193 return req.ErrorResponse(idx, fmt.Errorf("failed sve lookup: %s", err)) 194 } 195 existing, _ := existingRaw.(*structs.VariableEncrypted) 196 197 existingQuota, err := tx.First(TableVariablesQuotas, indexID, sv.Namespace) 198 if err != nil { 199 return req.ErrorResponse(idx, fmt.Errorf("variable quota lookup failed: %v", err)) 200 } 201 202 var quotaChange int64 203 204 // Set the CreateIndex and CreateTime 205 if existing != nil { 206 sv.CreateIndex = existing.CreateIndex 207 sv.CreateTime = existing.CreateTime 208 209 if existing.Equal(*sv) { 210 // Skip further writing in the state store if the entry is not actually 211 // changed. Nevertheless, the input's ModifyIndex should be reset 212 // since the TXN API returns a copy in the response. 213 sv.ModifyIndex = existing.ModifyIndex 214 sv.ModifyTime = existing.ModifyTime 215 return req.SuccessResponse(idx, nil) 216 } 217 sv.ModifyIndex = idx 218 quotaChange = int64(len(sv.Data) - len(existing.Data)) 219 } else { 220 sv.CreateIndex = idx 221 sv.ModifyIndex = idx 222 quotaChange = int64(len(sv.Data)) 223 } 224 225 if err := tx.Insert(TableVariables, sv); err != nil { 226 return req.ErrorResponse(idx, fmt.Errorf("failed inserting variable: %s", err)) 227 } 228 229 // Track quota usage 230 var quotaUsed *structs.VariablesQuota 231 if existingQuota != nil { 232 quotaUsed = existingQuota.(*structs.VariablesQuota) 233 quotaUsed = quotaUsed.Copy() 234 } else { 235 quotaUsed = &structs.VariablesQuota{ 236 Namespace: sv.Namespace, 237 CreateIndex: idx, 238 } 239 } 240 241 if quotaChange > math.MaxInt64-quotaUsed.Size { 242 // this limit is actually shared across all namespaces in the region's 243 // quota (if there is one), but we need this check here to prevent 244 // overflow as well 245 return req.ErrorResponse(idx, fmt.Errorf("variables can store a maximum of %d bytes of encrypted data per namespace", math.MaxInt)) 246 } 247 248 if quotaChange > 0 { 249 quotaUsed.Size += quotaChange 250 } else if quotaChange < 0 { 251 quotaUsed.Size -= helper.Min(quotaUsed.Size, -quotaChange) 252 } 253 254 err = s.enforceVariablesQuota(idx, tx, sv.Namespace, quotaChange) 255 if err != nil { 256 return req.ErrorResponse(idx, err) 257 } 258 259 // we check enforcement above even if there's no change because another 260 // namespace may have used up quota to make this no longer valid, but we 261 // only update the table if this namespace has changed 262 if quotaChange != 0 { 263 quotaUsed.ModifyIndex = idx 264 if err := tx.Insert(TableVariablesQuotas, quotaUsed); err != nil { 265 return req.ErrorResponse(idx, fmt.Errorf("variable quota insert failed: %v", err)) 266 } 267 } 268 269 if err := tx.Insert(tableIndex, 270 &IndexEntry{TableVariables, idx}); err != nil { 271 return req.ErrorResponse(idx, fmt.Errorf("failed updating variable index: %s", err)) 272 } 273 274 return req.SuccessResponse(idx, &sv.VariableMetadata) 275 } 276 277 // VarGet is used to retrieve a key/value pair from the state store. 278 func (s *StateStore) VarGet(ws memdb.WatchSet, namespace, path string) (uint64, *structs.VariableEncrypted, error) { 279 tx := s.db.ReadTxn() 280 defer tx.Abort() 281 282 return svGetTxn(tx, ws, namespace, path) 283 } 284 285 // svGetTxn is the inner method that gets a variable inside an existing 286 // transaction. 287 func svGetTxn(tx ReadTxn, 288 ws memdb.WatchSet, namespace, path string) (uint64, *structs.VariableEncrypted, error) { 289 290 // Get the table index. 291 idx := svMaxIndex(tx) 292 293 watchCh, entry, err := tx.FirstWatch(TableVariables, indexID, namespace, path) 294 if err != nil { 295 return 0, nil, fmt.Errorf("failed variable lookup: %s", err) 296 } 297 ws.Add(watchCh) 298 if entry != nil { 299 return idx, entry.(*structs.VariableEncrypted), nil 300 } 301 return idx, nil, nil 302 } 303 304 // VarDelete is used to delete a single variable in the 305 // the state store. 306 func (s *StateStore) VarDelete(idx uint64, req *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 307 tx := s.db.WriteTxn(idx) 308 defer tx.Abort() 309 310 // Perform the actual delete 311 resp := s.svDeleteTxn(tx, idx, req) 312 if !resp.IsOk() { 313 return resp 314 } 315 316 err := tx.Commit() 317 if err != nil { 318 return req.ErrorResponse(idx, err) 319 } 320 321 return resp 322 } 323 324 // VarDeleteCAS is used to conditionally delete a variable if and only if it has 325 // a given modify index. If the CAS index (cidx) specified is not equal to the 326 // last observed index for the given variable, then the call is a noop, 327 // otherwise a normal delete is invoked. 328 func (s *StateStore) VarDeleteCAS(idx uint64, req *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 329 tx := s.db.WriteTxn(idx) 330 defer tx.Abort() 331 332 resp := s.svDeleteCASTxn(tx, idx, req) 333 if !resp.IsOk() { 334 return resp 335 } 336 337 err := tx.Commit() 338 if err != nil { 339 return req.ErrorResponse(idx, err) 340 } 341 342 return resp 343 } 344 345 // svDeleteCASTxn is an inner method used to check the existing value 346 // of a variable within an existing transaction as part of a 347 // conditional delete. 348 func (s *StateStore) svDeleteCASTxn(tx WriteTxn, idx uint64, req *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 349 sv := req.Var 350 raw, err := tx.First(TableVariables, indexID, sv.Namespace, sv.Path) 351 if err != nil { 352 return req.ErrorResponse(idx, fmt.Errorf("failed variable lookup: %s", err)) 353 } 354 355 // ModifyIndex of 0 means that we are doing a delete-if-not-exists, so when 356 // raw == nil, it is successful. We should return here without manipulating 357 // the state store further. 358 if sv.ModifyIndex == 0 && raw == nil { 359 return req.SuccessResponse(idx, nil) 360 } 361 362 // If the ModifyIndex is set but the variable doesn't exist, return a 363 // plausible zero value as the conflict, because the user _expected_ there 364 // to have been a value and its absence is a conflict. 365 if sv.ModifyIndex != 0 && raw == nil { 366 zeroVal := &structs.VariableEncrypted{ 367 VariableMetadata: structs.VariableMetadata{ 368 Namespace: sv.Namespace, 369 Path: sv.Path, 370 }, 371 } 372 return req.ConflictResponse(idx, zeroVal) 373 } 374 375 // Any work beyond this point needs to be able to consult the actual 376 // returned content, so assert it back into the right type. 377 svEx, ok := raw.(*structs.VariableEncrypted) 378 379 // ModifyIndex of 0 means that we are doing a delete-if-not-exists, but 380 // there was a value stored in the state store 381 if sv.ModifyIndex == 0 && raw != nil { 382 return req.ConflictResponse(idx, svEx) 383 } 384 385 // If the existing index does not match the provided CAS index arg, then we 386 // shouldn't update anything and can safely return early here. 387 if !ok || sv.ModifyIndex != svEx.ModifyIndex { 388 return req.ConflictResponse(idx, svEx) 389 } 390 391 // Call the actual deletion if the above passed. 392 return s.svDeleteTxn(tx, idx, req) 393 } 394 395 // svDeleteTxn is the inner method used to perform the actual deletion 396 // of a variable within an existing transaction. 397 func (s *StateStore) svDeleteTxn(tx WriteTxn, idx uint64, req *structs.VarApplyStateRequest) *structs.VarApplyStateResponse { 398 399 // Look up the entry in the state store. 400 existingRaw, err := tx.First(TableVariables, indexID, req.Var.Namespace, req.Var.Path) 401 if err != nil { 402 return req.ErrorResponse(idx, fmt.Errorf("failed variable lookup: %s", err)) 403 } 404 if existingRaw == nil { 405 return req.SuccessResponse(idx, nil) 406 } 407 408 existingQuota, err := tx.First(TableVariablesQuotas, indexID, req.Var.Namespace) 409 if err != nil { 410 return req.ErrorResponse(idx, fmt.Errorf("variable quota lookup failed: %v", err)) 411 } 412 413 sv := existingRaw.(*structs.VariableEncrypted) 414 415 // Track quota usage 416 if existingQuota != nil { 417 quotaUsed := existingQuota.(*structs.VariablesQuota) 418 quotaUsed = quotaUsed.Copy() 419 quotaUsed.Size -= helper.Min(quotaUsed.Size, int64(len(sv.Data))) 420 quotaUsed.ModifyIndex = idx 421 if err := tx.Insert(TableVariablesQuotas, quotaUsed); err != nil { 422 return req.ErrorResponse(idx, fmt.Errorf("variable quota insert failed: %v", err)) 423 } 424 } 425 426 // Delete the variable and update the index table. 427 if err := tx.Delete(TableVariables, sv); err != nil { 428 return req.ErrorResponse(idx, fmt.Errorf("failed deleting variable entry: %s", err)) 429 } 430 431 if err := tx.Insert(tableIndex, &IndexEntry{TableVariables, idx}); err != nil { 432 return req.ErrorResponse(idx, fmt.Errorf("failed updating variable index: %s", err)) 433 } 434 435 return req.SuccessResponse(idx, nil) 436 } 437 438 // This extra indirection is to facilitate the tombstone case if it matters. 439 func svMaxIndex(tx ReadTxn) uint64 { 440 return maxIndexTxn(tx, TableVariables) 441 } 442 443 // WriteTxn is implemented by memdb.Txn to perform write operations. 444 type WriteTxn interface { 445 ReadTxn 446 Defer(func()) 447 Delete(table string, obj interface{}) error 448 DeleteAll(table, index string, args ...interface{}) (int, error) 449 DeletePrefix(table string, index string, prefix string) (bool, error) 450 Insert(table string, obj interface{}) error 451 } 452 453 // maxIndex is a helper used to retrieve the highest known index 454 // amongst a set of tables in the db. 455 func (s *StateStore) maxIndex(tables ...string) uint64 { 456 tx := s.db.ReadTxn() 457 defer tx.Abort() 458 return maxIndexTxn(tx, tables...) 459 } 460 461 // maxIndexTxn is a helper used to retrieve the highest known index 462 // amongst a set of tables in the db. 463 func maxIndexTxn(tx ReadTxn, tables ...string) uint64 { 464 return maxIndexWatchTxn(tx, nil, tables...) 465 } 466 467 func maxIndexWatchTxn(tx ReadTxn, ws memdb.WatchSet, tables ...string) uint64 { 468 var lindex uint64 469 for _, table := range tables { 470 ch, ti, err := tx.FirstWatch(tableIndex, "id", table) 471 if err != nil { 472 panic(fmt.Sprintf("unknown index: %s err: %s", table, err)) 473 } 474 if idx, ok := ti.(*IndexEntry); ok && idx.Value > lindex { 475 lindex = idx.Value 476 } 477 ws.Add(ch) 478 } 479 return lindex 480 } 481 482 // VariablesQuotas queries all the quotas and is used only for 483 // snapshot/restore and key rotation 484 func (s *StateStore) VariablesQuotas(ws memdb.WatchSet) (memdb.ResultIterator, error) { 485 txn := s.db.ReadTxn() 486 487 iter, err := txn.Get(TableVariablesQuotas, indexID) 488 if err != nil { 489 return nil, err 490 } 491 492 ws.Add(iter.WatchCh()) 493 return iter, nil 494 } 495 496 // VariablesQuotaByNamespace queries for quotas for a particular namespace 497 func (s *StateStore) VariablesQuotaByNamespace(ws memdb.WatchSet, namespace string) (*structs.VariablesQuota, error) { 498 txn := s.db.ReadTxn() 499 watchCh, raw, err := txn.FirstWatch(TableVariablesQuotas, indexID, namespace) 500 if err != nil { 501 return nil, fmt.Errorf("variable quota lookup failed: %v", err) 502 } 503 ws.Add(watchCh) 504 505 if raw == nil { 506 return nil, nil 507 } 508 quotaUsed := raw.(*structs.VariablesQuota) 509 return quotaUsed, nil 510 }