3db9f11c37
* Extend variables under the nomad path prefix to allow for job-templates (#15570) * Extend variables under the nomad path prefix to allow for job-templates * Add job-templates to error message hinting * RadioCard component for Job Templates (#15582) * chore: add * test: component API * ui: component template * refact: remove bc naming collission * styles: remove SASS var causing conflicts * Disallow specific variable at nomad/job-templates (#15681) * Disallows variables at exactly nomad/job-templates * idiomatic refactor * Expanding nomad job init to accept a template flag (#15571) * Adding a string flag for templates on job init * data-down actions-up version of a custom template editor within variable * Dont force grid on job template editor * list-templates flag started * Correctly slice from end of path name * Pre-review cleanup * Variable form acceptance test for job template editing * Some review cleanup * List Job templates test * Example from template test * Using must.assertions instead of require etc * ui: add choose template button (#15596) * ui: add new routes * chore: update file directory * ui: add choose template button * test: button and page navigation * refact: update var name * ui: use `Button` component from `HDS` (#15607) * ui: integrate buttons * refact: remove helper * ui: remove icons on non-tertiary buttons * refact: update normalize method for key/value pairs (#15612) * `revert`: `onCancel` for `JobDefinition` The `onCancel` method isn't included in the component API for `JobEditor` and the primary cancel behavior exists outside of the component. With the exception of the `JobDefinition` page where we include this button in the top right of the component instead of next to the `Plan` button. * style: increase button size * style: keep lime green * ui: select template (#15613) * ui: deprecate unused component * ui: deprecate tests * ui: jobs.run.templates.index * ui: update logic to handle templates * refact: revert key/value changes * style: padding for cards + buttons * temp: fixtures for mirage testing * Revert "refact: revert key/value changes" This reverts commit 124e95d12140be38fc921f7e15243034092c4063. * ui: guard template for unsaved job * ui: handle reading template variable * Revert "refact: update normalize method for key/value pairs (#15612)" This reverts commit 6f5ffc9b610702aee7c47fbff742cc81f819ab74. * revert: remove test fixtures * revert: prettier problems * refact: test doesnt need filter expression * styling: button sizes and responsive cards * refact: remove route guarding * ui: update variable adapter * refact: remove model editing behavior * refact: model should query variables to populate editor * ui: clear qp on exit * refact: cleanup deprecated API * refact: query all namespaces * refact: deprecate action * ui: rely on collection * refact: patch deprecate transition API * refact: patch test to expect namespace qp * styling: padding, conditionals * ui: flashMessage on 404 * test: update for o(n+1) query * ui: create new job template (#15744) * refact: remove unused code * refact: add type safety * test: select template flow * test: add data-test attrs * chore: remove dead code * test: create new job flow * ui: add create button * ui: create job template * refact: no need for wildcard * refact: record instead of delete * styling: spacing * ui: add error handling and form validation to job create template (#15767) * ui: handle server side errors * ui: show error to prevent duplicate * refact: conditional namespace * ui: save as template flow (#15787) * bug: patches failing tests associated with `pretender` (#15812) * refact: update assertion * refact: test set-up * ui: job templates manager view (#15815) * ui: manager list view * test: edit flow * refact: deprecate column-helper * ui: template edit and delete flow (#15823) * ui: manager list view * refact: update title * refact: update permissions * ui: template edit page * bug: typo * refact: update toast messages * bug: clear selections on exit (#15827) * bug: clear controllers on exit * test: mirage config changes (#15828) * refact: deprecate column-helper * style: update z-index for HDS * Revert "style: update z-index for HDS" This reverts commit d3d87ceab6d083f7164941587448607838944fc1. * refact: update delete button * refact: edit redirect * refact: patch reactivity issues * styling: fixed width * refact: override defaults * styling: edit text causing overflow * styling: add inline text Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com> * bug: edit `text` to `template` Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com> Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com> * test: delete flow job templates (#15896) * refact: edit names * bug: set correct ref to store * chore: trim whitespace: * test: delete flow * bug: reactively update view (#15904) * Initialized default jobs (#15856) * Initialized default jobs * More jobs scaffolded * Better commenting on a couple example job specs * Adapter doing the work * fall back to epic config * Label format helper and custom serialization logic * Test updates to account for a never-empty state * Test suite uses settled and maintain RecordArray in adapter return * Updates to hello-world and variables example jobspecs * Parameterized job gets optional payload output * Formatting changes for param and service discovery job templates * Multi-group service discovery job * Basic test for default templates (#15965) * Basic test for default templates * Percy snapshot for manage page * Some late-breaking design changes * Some copy edits to the header paragraphs for job templates (#15967) * Added some init options for job templates (#15994) * Async method for populating default job templates from the variable adapter --------- Co-authored-by: Jai <41024828+ChaiWithJai@users.noreply.github.com>
400 lines
11 KiB
Go
400 lines
11 KiB
Go
package structs
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// VariablesApplyRPCMethod is the RPC method for upserting or deleting a
|
|
// variable by its namespace and path, with optional conflict detection.
|
|
//
|
|
// Args: VariablesApplyRequest
|
|
// Reply: VariablesApplyResponse
|
|
VariablesApplyRPCMethod = "Variables.Apply"
|
|
|
|
// VariablesListRPCMethod is the RPC method for listing variables within
|
|
// Nomad.
|
|
//
|
|
// Args: VariablesListRequest
|
|
// Reply: VariablesListResponse
|
|
VariablesListRPCMethod = "Variables.List"
|
|
|
|
// VariablesReadRPCMethod is the RPC method for fetching a variable
|
|
// according to its namepace and path.
|
|
//
|
|
// Args: VariablesByNameRequest
|
|
// Reply: VariablesByNameResponse
|
|
VariablesReadRPCMethod = "Variables.Read"
|
|
|
|
// maxVariableSize is the maximum size of the unencrypted contents of a
|
|
// variable. This size is deliberately set low and is not configurable, to
|
|
// discourage DoS'ing the cluster
|
|
maxVariableSize = 65536
|
|
)
|
|
|
|
// VariableMetadata is the metadata envelope for a Variable, it is the list
|
|
// object and is shared data between an VariableEncrypted and a
|
|
// VariableDecrypted object.
|
|
type VariableMetadata struct {
|
|
Namespace string
|
|
Path string
|
|
CreateIndex uint64
|
|
CreateTime int64
|
|
ModifyIndex uint64
|
|
ModifyTime int64
|
|
}
|
|
|
|
// VariableEncrypted structs are returned from the Encrypter's encrypt
|
|
// method. They are the only form that should ever be persisted to storage.
|
|
type VariableEncrypted struct {
|
|
VariableMetadata
|
|
VariableData
|
|
}
|
|
|
|
// VariableData is the secret data for a Variable
|
|
type VariableData struct {
|
|
Data []byte // includes nonce
|
|
KeyID string // ID of root key used to encrypt this entry
|
|
}
|
|
|
|
// VariableDecrypted structs are returned from the Encrypter's decrypt
|
|
// method. Since they contains sensitive material, they should never be
|
|
// persisted to disk.
|
|
type VariableDecrypted struct {
|
|
VariableMetadata
|
|
Items VariableItems
|
|
}
|
|
|
|
// VariableItems are the actual secrets stored in a variable. They are always
|
|
// encrypted and decrypted as a single unit.
|
|
type VariableItems map[string]string
|
|
|
|
func (svi VariableItems) Size() uint64 {
|
|
var out uint64
|
|
for k, v := range svi {
|
|
out += uint64(len(k))
|
|
out += uint64(len(v))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Equal checks both the metadata and items in a VariableDecrypted struct
|
|
func (v1 VariableDecrypted) Equal(v2 VariableDecrypted) bool {
|
|
return v1.VariableMetadata.Equal(v2.VariableMetadata) &&
|
|
v1.Items.Equal(v2.Items)
|
|
}
|
|
|
|
// Equal is a convenience method to provide similar equality checking syntax
|
|
// for metadata and the VariablesData or VariableItems struct
|
|
func (sv VariableMetadata) Equal(sv2 VariableMetadata) bool {
|
|
return sv == sv2
|
|
}
|
|
|
|
// Equal performs deep equality checking on the cleartext items of a
|
|
// VariableDecrypted. Uses reflect.DeepEqual
|
|
func (i1 VariableItems) Equal(i2 VariableItems) bool {
|
|
return reflect.DeepEqual(i1, i2)
|
|
}
|
|
|
|
// Equal checks both the metadata and encrypted data for a VariableEncrypted
|
|
// struct
|
|
func (v1 VariableEncrypted) Equal(v2 VariableEncrypted) bool {
|
|
return v1.VariableMetadata.Equal(v2.VariableMetadata) &&
|
|
v1.VariableData.Equal(v2.VariableData)
|
|
}
|
|
|
|
// Equal performs deep equality checking on the encrypted data part of a
|
|
// VariableEncrypted
|
|
func (d1 VariableData) Equal(d2 VariableData) bool {
|
|
return d1.KeyID == d2.KeyID &&
|
|
bytes.Equal(d1.Data, d2.Data)
|
|
}
|
|
|
|
func (sv VariableDecrypted) Copy() VariableDecrypted {
|
|
return VariableDecrypted{
|
|
VariableMetadata: sv.VariableMetadata,
|
|
Items: sv.Items.Copy(),
|
|
}
|
|
}
|
|
|
|
func (sv VariableItems) Copy() VariableItems {
|
|
out := make(VariableItems, len(sv))
|
|
for k, v := range sv {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (sv VariableEncrypted) Copy() VariableEncrypted {
|
|
return VariableEncrypted{
|
|
VariableMetadata: sv.VariableMetadata,
|
|
VariableData: sv.VariableData.Copy(),
|
|
}
|
|
}
|
|
|
|
func (sv VariableData) Copy() VariableData {
|
|
out := make([]byte, len(sv.Data))
|
|
copy(out, sv.Data)
|
|
return VariableData{
|
|
Data: out,
|
|
KeyID: sv.KeyID,
|
|
}
|
|
}
|
|
|
|
var (
|
|
// validVariablePath is used to validate a variable path. We restrict to
|
|
// RFC3986 URL-safe characters that don't conflict with the use of
|
|
// characters "@" and "." in template blocks. We also restrict the length so
|
|
// that a user can't make queries in the state store unusually expensive (as
|
|
// they are O(k) on the key length)
|
|
validVariablePath = regexp.MustCompile("^[a-zA-Z0-9-_~/]{1,128}$")
|
|
)
|
|
|
|
func (v VariableDecrypted) Validate() error {
|
|
|
|
if v.Namespace == AllNamespacesSentinel {
|
|
return errors.New("can not target wildcard (\"*\")namespace")
|
|
}
|
|
|
|
if len(v.Items) == 0 {
|
|
return errors.New("empty variables are invalid")
|
|
}
|
|
if v.Items.Size() > maxVariableSize {
|
|
return errors.New("variables are limited to 64KiB in total size")
|
|
}
|
|
|
|
if err := validatePath(v.Path); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validatePath(path string) error {
|
|
if len(path) == 0 {
|
|
return fmt.Errorf("variable requires path")
|
|
}
|
|
if !validVariablePath.MatchString(path) {
|
|
return fmt.Errorf("invalid path %q", path)
|
|
}
|
|
|
|
parts := strings.Split(path, "/")
|
|
|
|
if parts[0] != "nomad" {
|
|
return nil
|
|
}
|
|
|
|
// Don't allow a variable with path "nomad"
|
|
if len(parts) == 1 {
|
|
return fmt.Errorf("\"nomad\" is a reserved top-level directory path, but you may write variables to \"nomad/jobs\", \"nomad/job-templates\", or below")
|
|
}
|
|
|
|
switch {
|
|
case parts[1] == "jobs":
|
|
// Any path including "nomad/jobs" is valid
|
|
return nil
|
|
case parts[1] == "job-templates" && len(parts) == 3:
|
|
// Paths including "nomad/job-templates" is valid, provided they have single further path part
|
|
return nil
|
|
case parts[1] == "job-templates":
|
|
// Disallow exactly nomad/job-templates with no further paths
|
|
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\"")
|
|
default:
|
|
// Disallow arbitrary sub-paths beneath nomad/
|
|
return fmt.Errorf("only paths at \"nomad/jobs\" or \"nomad/job-templates\" and below are valid paths under the top-level \"nomad\" directory")
|
|
}
|
|
}
|
|
|
|
func (sv *VariableDecrypted) Canonicalize() {
|
|
if sv.Namespace == "" {
|
|
sv.Namespace = DefaultNamespace
|
|
}
|
|
}
|
|
|
|
// GetNamespace returns the variable's namespace. Used for pagination.
|
|
func (sv *VariableMetadata) Copy() *VariableMetadata {
|
|
var out VariableMetadata = *sv
|
|
return &out
|
|
}
|
|
|
|
// GetNamespace returns the variable's namespace. Used for pagination.
|
|
func (sv VariableMetadata) GetNamespace() string {
|
|
return sv.Namespace
|
|
}
|
|
|
|
// GetID returns the variable's path. Used for pagination.
|
|
func (sv VariableMetadata) GetID() string {
|
|
return sv.Path
|
|
}
|
|
|
|
// GetCreateIndex returns the variable's create index. Used for pagination.
|
|
func (sv VariableMetadata) GetCreateIndex() uint64 {
|
|
return sv.CreateIndex
|
|
}
|
|
|
|
// VariablesQuota is used to track the total size of variables entries per
|
|
// namespace. The total length of Variable.EncryptedData in bytes will be added
|
|
// to the VariablesQuota table in the same transaction as a write, update, or
|
|
// delete. This tracking effectively caps the maximum size of variables in a
|
|
// given namespace to MaxInt64 bytes.
|
|
type VariablesQuota struct {
|
|
Namespace string
|
|
Size int64
|
|
CreateIndex uint64
|
|
ModifyIndex uint64
|
|
}
|
|
|
|
func (svq *VariablesQuota) Copy() *VariablesQuota {
|
|
if svq == nil {
|
|
return nil
|
|
}
|
|
nq := new(VariablesQuota)
|
|
*nq = *svq
|
|
return nq
|
|
}
|
|
|
|
// ---------------------------------------
|
|
// RPC and FSM request/response objects
|
|
|
|
// VarOp constants give possible operations available in a transaction.
|
|
type VarOp string
|
|
|
|
const (
|
|
VarOpSet VarOp = "set"
|
|
VarOpDelete VarOp = "delete"
|
|
VarOpDeleteCAS VarOp = "delete-cas"
|
|
VarOpCAS VarOp = "cas"
|
|
)
|
|
|
|
// VarOpResult constants give possible operations results from a transaction.
|
|
type VarOpResult string
|
|
|
|
const (
|
|
VarOpResultOk VarOpResult = "ok"
|
|
VarOpResultConflict VarOpResult = "conflict"
|
|
VarOpResultRedacted VarOpResult = "conflict-redacted"
|
|
VarOpResultError VarOpResult = "error"
|
|
)
|
|
|
|
// VariablesApplyRequest is used by users to operate on the variable store
|
|
type VariablesApplyRequest struct {
|
|
Op VarOp // Operation to be performed during apply
|
|
Var *VariableDecrypted // Variable-shaped request data
|
|
WriteRequest
|
|
}
|
|
|
|
// VariablesApplyResponse is sent back to the user to inform them of success or failure
|
|
type VariablesApplyResponse struct {
|
|
Op VarOp // Operation performed
|
|
Input *VariableDecrypted // Input supplied
|
|
Result VarOpResult // Return status from operation
|
|
Error error // Error if any
|
|
Conflict *VariableDecrypted // Conflicting value if applicable
|
|
Output *VariableDecrypted // Operation Result if successful; nil for successful deletes
|
|
WriteMeta
|
|
}
|
|
|
|
func (r *VariablesApplyResponse) IsOk() bool {
|
|
return r.Result == VarOpResultOk
|
|
}
|
|
|
|
func (r *VariablesApplyResponse) IsConflict() bool {
|
|
return r.Result == VarOpResultConflict || r.Result == VarOpResultRedacted
|
|
}
|
|
|
|
func (r *VariablesApplyResponse) IsError() bool {
|
|
return r.Result == VarOpResultError
|
|
}
|
|
|
|
func (r *VariablesApplyResponse) IsRedacted() bool {
|
|
return r.Result == VarOpResultRedacted
|
|
}
|
|
|
|
// VarApplyStateRequest is used by the FSM to modify the variable store
|
|
type VarApplyStateRequest struct {
|
|
Op VarOp // Which operation are we performing
|
|
Var *VariableEncrypted // Which directory entry
|
|
WriteRequest
|
|
}
|
|
|
|
// VarApplyStateResponse is used by the FSM to inform the RPC layer of success or failure
|
|
type VarApplyStateResponse struct {
|
|
Op VarOp // Which operation were we performing
|
|
Result VarOpResult // What happened (ok, conflict, error)
|
|
Error error // error if any
|
|
Conflict *VariableEncrypted // conflicting variable if applies
|
|
WrittenSVMeta *VariableMetadata // for making the VariablesApplyResponse
|
|
WriteMeta
|
|
}
|
|
|
|
func (r *VarApplyStateRequest) ErrorResponse(raftIndex uint64, err error) *VarApplyStateResponse {
|
|
return &VarApplyStateResponse{
|
|
Op: r.Op,
|
|
Result: VarOpResultError,
|
|
Error: err,
|
|
WriteMeta: WriteMeta{Index: raftIndex},
|
|
}
|
|
}
|
|
|
|
func (r *VarApplyStateRequest) SuccessResponse(raftIndex uint64, meta *VariableMetadata) *VarApplyStateResponse {
|
|
return &VarApplyStateResponse{
|
|
Op: r.Op,
|
|
Result: VarOpResultOk,
|
|
WrittenSVMeta: meta,
|
|
WriteMeta: WriteMeta{Index: raftIndex},
|
|
}
|
|
}
|
|
|
|
func (r *VarApplyStateRequest) ConflictResponse(raftIndex uint64, cv *VariableEncrypted) *VarApplyStateResponse {
|
|
var cvCopy VariableEncrypted
|
|
if cv != nil {
|
|
// make a copy so that we aren't sending
|
|
// the live state store version
|
|
cvCopy = cv.Copy()
|
|
}
|
|
return &VarApplyStateResponse{
|
|
Op: r.Op,
|
|
Result: VarOpResultConflict,
|
|
Conflict: &cvCopy,
|
|
WriteMeta: WriteMeta{Index: raftIndex},
|
|
}
|
|
}
|
|
|
|
func (r *VarApplyStateResponse) IsOk() bool {
|
|
return r.Result == VarOpResultOk
|
|
}
|
|
|
|
func (r *VarApplyStateResponse) IsConflict() bool {
|
|
return r.Result == VarOpResultConflict
|
|
}
|
|
|
|
func (r *VarApplyStateResponse) IsError() bool {
|
|
// FIXME: This is brittle and requires immense faith that
|
|
// the response is properly managed.
|
|
return r.Result == VarOpResultError
|
|
}
|
|
|
|
type VariablesListRequest struct {
|
|
QueryOptions
|
|
}
|
|
|
|
type VariablesListResponse struct {
|
|
Data []*VariableMetadata
|
|
QueryMeta
|
|
}
|
|
|
|
type VariablesReadRequest struct {
|
|
Path string
|
|
QueryOptions
|
|
}
|
|
|
|
type VariablesReadResponse struct {
|
|
Data *VariableDecrypted
|
|
QueryMeta
|
|
}
|