open-nomad/nomad/structs/variables.go
Phil Renaud 3db9f11c37
[feat] Nomad Job Templates (#15746)
* 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>
2023-02-02 10:37:40 -05:00

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
}