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