github.com/hernad/nomad@v1.6.112/nomad/structs/variables.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package structs 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "reflect" 11 "regexp" 12 "strings" 13 ) 14 15 const ( 16 // VariablesApplyRPCMethod is the RPC method for upserting or deleting a 17 // variable by its namespace and path, with optional conflict detection. 18 // 19 // Args: VariablesApplyRequest 20 // Reply: VariablesApplyResponse 21 VariablesApplyRPCMethod = "Variables.Apply" 22 23 // VariablesListRPCMethod is the RPC method for listing variables within 24 // Nomad. 25 // 26 // Args: VariablesListRequest 27 // Reply: VariablesListResponse 28 VariablesListRPCMethod = "Variables.List" 29 30 // VariablesReadRPCMethod is the RPC method for fetching a variable 31 // according to its namepace and path. 32 // 33 // Args: VariablesByNameRequest 34 // Reply: VariablesByNameResponse 35 VariablesReadRPCMethod = "Variables.Read" 36 37 // maxVariableSize is the maximum size of the unencrypted contents of a 38 // variable. This size is deliberately set low and is not configurable, to 39 // discourage DoS'ing the cluster 40 maxVariableSize = 65536 41 ) 42 43 // VariableMetadata is the metadata envelope for a Variable, it is the list 44 // object and is shared data between an VariableEncrypted and a 45 // VariableDecrypted object. 46 type VariableMetadata struct { 47 Namespace string 48 Path string 49 CreateIndex uint64 50 CreateTime int64 51 ModifyIndex uint64 52 ModifyTime int64 53 } 54 55 // VariableEncrypted structs are returned from the Encrypter's encrypt 56 // method. They are the only form that should ever be persisted to storage. 57 type VariableEncrypted struct { 58 VariableMetadata 59 VariableData 60 } 61 62 // VariableData is the secret data for a Variable 63 type VariableData struct { 64 Data []byte // includes nonce 65 KeyID string // ID of root key used to encrypt this entry 66 } 67 68 // VariableDecrypted structs are returned from the Encrypter's decrypt 69 // method. Since they contains sensitive material, they should never be 70 // persisted to disk. 71 type VariableDecrypted struct { 72 VariableMetadata 73 Items VariableItems 74 } 75 76 // VariableItems are the actual secrets stored in a variable. They are always 77 // encrypted and decrypted as a single unit. 78 type VariableItems map[string]string 79 80 func (vi VariableItems) Size() uint64 { 81 var out uint64 82 for k, v := range vi { 83 out += uint64(len(k)) 84 out += uint64(len(v)) 85 } 86 return out 87 } 88 89 // Equal checks both the metadata and items in a VariableDecrypted struct 90 func (vd VariableDecrypted) Equal(v2 VariableDecrypted) bool { 91 return vd.VariableMetadata.Equal(v2.VariableMetadata) && 92 vd.Items.Equal(v2.Items) 93 } 94 95 // Equal is a convenience method to provide similar equality checking syntax 96 // for metadata and the VariablesData or VariableItems struct 97 func (sv VariableMetadata) Equal(sv2 VariableMetadata) bool { 98 return sv == sv2 99 } 100 101 // Equal performs deep equality checking on the cleartext items of a 102 // VariableDecrypted. Uses reflect.DeepEqual 103 func (vi VariableItems) Equal(i2 VariableItems) bool { 104 return reflect.DeepEqual(vi, i2) 105 } 106 107 // Equal checks both the metadata and encrypted data for a VariableEncrypted 108 // struct 109 func (ve VariableEncrypted) Equal(v2 VariableEncrypted) bool { 110 return ve.VariableMetadata.Equal(v2.VariableMetadata) && 111 ve.VariableData.Equal(v2.VariableData) 112 } 113 114 // Equal performs deep equality checking on the encrypted data part of a 115 // VariableEncrypted 116 func (vd VariableData) Equal(d2 VariableData) bool { 117 return vd.KeyID == d2.KeyID && 118 bytes.Equal(vd.Data, d2.Data) 119 } 120 121 func (vd VariableDecrypted) Copy() VariableDecrypted { 122 return VariableDecrypted{ 123 VariableMetadata: vd.VariableMetadata, 124 Items: vd.Items.Copy(), 125 } 126 } 127 128 func (vi VariableItems) Copy() VariableItems { 129 out := make(VariableItems, len(vi)) 130 for k, v := range vi { 131 out[k] = v 132 } 133 return out 134 } 135 136 func (ve VariableEncrypted) Copy() VariableEncrypted { 137 return VariableEncrypted{ 138 VariableMetadata: ve.VariableMetadata, 139 VariableData: ve.VariableData.Copy(), 140 } 141 } 142 143 func (vd VariableData) Copy() VariableData { 144 out := make([]byte, len(vd.Data)) 145 copy(out, vd.Data) 146 return VariableData{ 147 Data: out, 148 KeyID: vd.KeyID, 149 } 150 } 151 152 var ( 153 // validVariablePath is used to validate a variable path. We restrict to 154 // RFC3986 URL-safe characters that don't conflict with the use of 155 // characters "@" and "." in template blocks. We also restrict the length so 156 // that a user can't make queries in the state store unusually expensive (as 157 // they are O(k) on the key length) 158 validVariablePath = regexp.MustCompile("^[a-zA-Z0-9-_~/]{1,128}$") 159 ) 160 161 func (vd VariableDecrypted) Validate() error { 162 163 if vd.Namespace == AllNamespacesSentinel { 164 return errors.New("can not target wildcard (\"*\")namespace") 165 } 166 167 if len(vd.Items) == 0 { 168 return errors.New("empty variables are invalid") 169 } 170 if vd.Items.Size() > maxVariableSize { 171 return errors.New("variables are limited to 64KiB in total size") 172 } 173 174 if err := validatePath(vd.Path); err != nil { 175 return err 176 } 177 178 return nil 179 } 180 181 func validatePath(path string) error { 182 if len(path) == 0 { 183 return fmt.Errorf("variable requires path") 184 } 185 if !validVariablePath.MatchString(path) { 186 return fmt.Errorf("invalid path %q", path) 187 } 188 189 parts := strings.Split(path, "/") 190 191 if parts[0] != "nomad" { 192 return nil 193 } 194 195 // Don't allow a variable with path "nomad" 196 if len(parts) == 1 { 197 return fmt.Errorf("\"nomad\" is a reserved top-level directory path, but you may write variables to \"nomad/jobs\", \"nomad/job-templates\", or below") 198 } 199 200 switch { 201 case parts[1] == "jobs": 202 // Any path including "nomad/jobs" is valid 203 return nil 204 case parts[1] == "job-templates" && len(parts) == 3: 205 // Paths including "nomad/job-templates" is valid, provided they have single further path part 206 return nil 207 case parts[1] == "job-templates": 208 // Disallow exactly nomad/job-templates with no further paths 209 return fmt.Errorf("\"nomad/job-templates\" is a reserved directory path, but you may write variables at the level below it, for example, \"nomad/job-templates/template-name\"") 210 default: 211 // Disallow arbitrary sub-paths beneath nomad/ 212 return fmt.Errorf("only paths at \"nomad/jobs\" or \"nomad/job-templates\" and below are valid paths under the top-level \"nomad\" directory") 213 } 214 } 215 216 func (vd *VariableDecrypted) Canonicalize() { 217 if vd.Namespace == "" { 218 vd.Namespace = DefaultNamespace 219 } 220 } 221 222 // Copy returns a fully hydrated copy of VariableMetadata that can be 223 // manipulated while ensuring the original is not touched. 224 func (sv *VariableMetadata) Copy() *VariableMetadata { 225 var out = *sv 226 return &out 227 } 228 229 // GetNamespace returns the variable's namespace. Used for pagination. 230 func (sv VariableMetadata) GetNamespace() string { 231 return sv.Namespace 232 } 233 234 // GetID returns the variable's path. Used for pagination. 235 func (sv VariableMetadata) GetID() string { 236 return sv.Path 237 } 238 239 // GetCreateIndex returns the variable's create index. Used for pagination. 240 func (sv VariableMetadata) GetCreateIndex() uint64 { 241 return sv.CreateIndex 242 } 243 244 // VariablesQuota is used to track the total size of variables entries per 245 // namespace. The total length of Variable.EncryptedData in bytes will be added 246 // to the VariablesQuota table in the same transaction as a write, update, or 247 // delete. This tracking effectively caps the maximum size of variables in a 248 // given namespace to MaxInt64 bytes. 249 type VariablesQuota struct { 250 Namespace string 251 Size int64 252 CreateIndex uint64 253 ModifyIndex uint64 254 } 255 256 func (svq *VariablesQuota) Copy() *VariablesQuota { 257 if svq == nil { 258 return nil 259 } 260 nq := new(VariablesQuota) 261 *nq = *svq 262 return nq 263 } 264 265 // --------------------------------------- 266 // RPC and FSM request/response objects 267 268 // VarOp constants give possible operations available in a transaction. 269 type VarOp string 270 271 const ( 272 VarOpSet VarOp = "set" 273 VarOpDelete VarOp = "delete" 274 VarOpDeleteCAS VarOp = "delete-cas" 275 VarOpCAS VarOp = "cas" 276 ) 277 278 // VarOpResult constants give possible operations results from a transaction. 279 type VarOpResult string 280 281 const ( 282 VarOpResultOk VarOpResult = "ok" 283 VarOpResultConflict VarOpResult = "conflict" 284 VarOpResultRedacted VarOpResult = "conflict-redacted" 285 VarOpResultError VarOpResult = "error" 286 ) 287 288 // VariablesApplyRequest is used by users to operate on the variable store 289 type VariablesApplyRequest struct { 290 Op VarOp // Operation to be performed during apply 291 Var *VariableDecrypted // Variable-shaped request data 292 WriteRequest 293 } 294 295 // VariablesApplyResponse is sent back to the user to inform them of success or failure 296 type VariablesApplyResponse struct { 297 Op VarOp // Operation performed 298 Input *VariableDecrypted // Input supplied 299 Result VarOpResult // Return status from operation 300 Error error // Error if any 301 Conflict *VariableDecrypted // Conflicting value if applicable 302 Output *VariableDecrypted // Operation Result if successful; nil for successful deletes 303 WriteMeta 304 } 305 306 func (r *VariablesApplyResponse) IsOk() bool { 307 return r.Result == VarOpResultOk 308 } 309 310 func (r *VariablesApplyResponse) IsConflict() bool { 311 return r.Result == VarOpResultConflict || r.Result == VarOpResultRedacted 312 } 313 314 func (r *VariablesApplyResponse) IsError() bool { 315 return r.Result == VarOpResultError 316 } 317 318 func (r *VariablesApplyResponse) IsRedacted() bool { 319 return r.Result == VarOpResultRedacted 320 } 321 322 // VarApplyStateRequest is used by the FSM to modify the variable store 323 type VarApplyStateRequest struct { 324 Op VarOp // Which operation are we performing 325 Var *VariableEncrypted // Which directory entry 326 WriteRequest 327 } 328 329 // VarApplyStateResponse is used by the FSM to inform the RPC layer of success or failure 330 type VarApplyStateResponse struct { 331 Op VarOp // Which operation were we performing 332 Result VarOpResult // What happened (ok, conflict, error) 333 Error error // error if any 334 Conflict *VariableEncrypted // conflicting variable if applies 335 WrittenSVMeta *VariableMetadata // for making the VariablesApplyResponse 336 WriteMeta 337 } 338 339 func (r *VarApplyStateRequest) ErrorResponse(raftIndex uint64, err error) *VarApplyStateResponse { 340 return &VarApplyStateResponse{ 341 Op: r.Op, 342 Result: VarOpResultError, 343 Error: err, 344 WriteMeta: WriteMeta{Index: raftIndex}, 345 } 346 } 347 348 func (r *VarApplyStateRequest) SuccessResponse(raftIndex uint64, meta *VariableMetadata) *VarApplyStateResponse { 349 return &VarApplyStateResponse{ 350 Op: r.Op, 351 Result: VarOpResultOk, 352 WrittenSVMeta: meta, 353 WriteMeta: WriteMeta{Index: raftIndex}, 354 } 355 } 356 357 func (r *VarApplyStateRequest) ConflictResponse(raftIndex uint64, cv *VariableEncrypted) *VarApplyStateResponse { 358 var cvCopy VariableEncrypted 359 if cv != nil { 360 // make a copy so that we aren't sending 361 // the live state store version 362 cvCopy = cv.Copy() 363 } 364 return &VarApplyStateResponse{ 365 Op: r.Op, 366 Result: VarOpResultConflict, 367 Conflict: &cvCopy, 368 WriteMeta: WriteMeta{Index: raftIndex}, 369 } 370 } 371 372 func (r *VarApplyStateResponse) IsOk() bool { 373 return r.Result == VarOpResultOk 374 } 375 376 func (r *VarApplyStateResponse) IsConflict() bool { 377 return r.Result == VarOpResultConflict 378 } 379 380 func (r *VarApplyStateResponse) IsError() bool { 381 // FIXME: This is brittle and requires immense faith that 382 // the response is properly managed. 383 return r.Result == VarOpResultError 384 } 385 386 type VariablesListRequest struct { 387 QueryOptions 388 } 389 390 type VariablesListResponse struct { 391 Data []*VariableMetadata 392 QueryMeta 393 } 394 395 type VariablesReadRequest struct { 396 Path string 397 QueryOptions 398 } 399 400 type VariablesReadResponse struct { 401 Data *VariableDecrypted 402 QueryMeta 403 }