Skip to content

Commit

Permalink
Scaffolding actions (#18639)
Browse files Browse the repository at this point in the history
* Task-level actions for job submissions and retrieval

* FIXME: Temporary workaround to get ember dev server to pass exec through to 4646

* Update api/tasks.go

Co-authored-by: Tim Gross <[email protected]>

* Update command/agent/job_endpoint.go

Co-authored-by: Tim Gross <[email protected]>

* Diff and copy implementations

* Action structs get their own file, diff updates to behave like our other diffs

* Test to observe actions changes in a version update

* Tests migrated into structs/diff_test and modified with PR comments in mind

* APIActionToSTructsAction now returns a new value

* de-comment some plain parts, remove unused action lookup

* unused param in action converter

---------

Co-authored-by: Tim Gross <[email protected]>
  • Loading branch information
philrenaud and tgross authored Oct 11, 2023
1 parent 7267be7 commit a0a56c9
Show file tree
Hide file tree
Showing 13 changed files with 389 additions and 5 deletions.
8 changes: 8 additions & 0 deletions api/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ type Task struct {

// Workload Identities
Identities []*WorkloadIdentity `hcl:"identity,block"`

Actions []*Action `hcl:"action,block"`
}

func (t *Task) Canonicalize(tg *TaskGroup, job *Job) {
Expand Down Expand Up @@ -1167,3 +1169,9 @@ type WorkloadIdentity struct {
ServiceName string `hcl:"service_name,optional"`
TTL time.Duration `mapstructure:"ttl" hcl:"ttl,optional"`
}

type Action struct {
Name string `hcl:"name,label"`
Command string `mapstructure:"command" hcl:"command"`
Args []string `mapstructure:"args" hcl:"args,optional"`
}
14 changes: 13 additions & 1 deletion command/agent/alloc_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,19 @@ func (s *HTTPServer) allocExec(allocID string, resp http.ResponseWriter, req *ht
}
s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)

conn, err := s.wsUpgrader.Upgrade(resp, req, nil)
// conn, err := s.wsUpgrader.Upgrade(resp, req, nil)
// FIXME: this is an open checkOrigin here that allows :4200 to make requests to :4646,
// freeing local ember up from not having to proxy.
// This is like three workarounds in a trenchcoat and I dno't feel good about it but it unblocks me

var upgrader = websocket.Upgrader{
// Allow all origins
CheckOrigin: func(r *http.Request) bool { return true },
}

// Then when you upgrade the connection:
conn, err := upgrader.Upgrade(resp, req, nil)

if err != nil {
return nil, fmt.Errorf("failed to upgrade connection: %v", err)
}
Expand Down
13 changes: 13 additions & 0 deletions command/agent/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,11 @@ func ApiTaskToStructsTask(job *structs.Job, group *structs.TaskGroup,
Sidecar: apiTask.Lifecycle.Sidecar,
}
}

for _, action := range apiTask.Actions {
act := ApiActionToStructsAction(job, action)
structsTask.Actions = append(structsTask.Actions, act)
}
}

// apiWaitConfigToStructsWaitConfig is a copy and type conversion between the API
Expand Down Expand Up @@ -1385,6 +1390,14 @@ func ApiCSIPluginConfigToStructsCSIPluginConfig(apiConfig *api.TaskCSIPluginConf
return sc
}

func ApiActionToStructsAction(job *structs.Job, action *api.Action) *structs.Action {
return &structs.Action{
Name: action.Name,
Args: slices.Clone(action.Args),
Command: action.Command,
}
}

func ApiResourcesToStructs(in *api.Resources) *structs.Resources {
if in == nil {
return nil
Expand Down
13 changes: 13 additions & 0 deletions command/agent/testingutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ func MockJob() *api.Job {
PortLabel: "admin",
},
},
// actions
Actions: []*api.Action{
{
Name: "date test",
Command: "/bin/date",
Args: []string{"-u"},
},
{
Name: "echo test",
Command: "/bin/echo",
Args: []string{"hello world"},
},
},
LogConfig: api.DefaultLogConfig(),
Resources: &api.Resources{
CPU: pointer.Of(500),
Expand Down
1 change: 1 addition & 0 deletions jobspec/parse_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var (
"kind",
"volume_mount",
"csi_plugin",
"actions",
)

sidecarTaskKeys = append(commonTaskKeys,
Expand Down
12 changes: 12 additions & 0 deletions nomad/mock/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ func Job() *structs.Job {
Env: map[string]string{
"FOO": "bar",
},
Actions: []*structs.Action{
{
Name: "date test",
Command: "/bin/date",
Args: []string{"-u"},
},
{
Name: "echo test",
Command: "/bin/echo",
Args: []string{"hello world"},
},
},
Services: []*structs.Service{
{
Name: "${TASK}-frontend",
Expand Down
38 changes: 38 additions & 0 deletions nomad/structs/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

// Actions are executable commands that can be run on an allocation within
// the context of a task. They are left open-ended enough to be applied to
// other Nomad concepts like Nodes in the future.

package structs

import "slices"

type Action struct {
Name string
Command string
Args []string
}

func (a *Action) Copy() *Action {
if a == nil {
return nil
}
na := new(Action)
*na = *a
na.Args = slices.Clone(a.Args)
return na
}

func (a *Action) Equal(o *Action) bool {
if a == o {
return true
}
if a == nil || o == nil {
return false
}
return a.Name == o.Name &&
a.Command == o.Command &&
slices.Equal(a.Args, o.Args)
}
67 changes: 67 additions & 0 deletions nomad/structs/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,76 @@ func (t *Task) Diff(other *Task, contextual bool) (*TaskDiff, error) {
diff.Objects = append(diff.Objects, altIDDiffs...)
}

// Actions diff
if aDiffs := actionDiffs(t.Actions, other.Actions, contextual); aDiffs != nil {
diff.Objects = append(diff.Objects, aDiffs...)
}

return diff, nil
}

func actionDiff(old, new *Action, contextual bool) *ObjectDiff {
diff := &ObjectDiff{Type: DiffTypeNone, Name: "Action"}
var oldPrimitiveFlat, newPrimitiveFlat map[string]string

if reflect.DeepEqual(old, new) {
return nil
} else if old == nil {
old = &Action{}
diff.Type = DiffTypeAdded
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
} else if new == nil {
new = &Action{}
diff.Type = DiffTypeDeleted
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
} else {
diff.Type = DiffTypeEdited
oldPrimitiveFlat = flatmap.Flatten(old, nil, true)
newPrimitiveFlat = flatmap.Flatten(new, nil, true)
}

// Diff the primitive fields
diff.Fields = fieldDiffs(oldPrimitiveFlat, newPrimitiveFlat, contextual)

// Diff the Args field using stringSetDiff
if setDiff := stringSetDiff(old.Args, new.Args, "Args", contextual); setDiff != nil {
diff.Objects = append(diff.Objects, setDiff)
}

return diff
}

// actionDiffs diffs a set of actions. If contextual diff is enabled, unchanged
// fields within objects nested in the actions will be returned.
func actionDiffs(old, new []*Action, contextual bool) []*ObjectDiff {
var diffs []*ObjectDiff

for i := 0; i < len(old) && i < len(new); i++ {
oldAction := old[i]
newAction := new[i]

if diff := actionDiff(oldAction, newAction, contextual); diff != nil {
diffs = append(diffs, diff)
}
}

for i := len(new); i < len(old); i++ {
if diff := actionDiff(old[i], nil, contextual); diff != nil {
diffs = append(diffs, diff)
}
}

for i := len(old); i < len(new); i++ {
if diff := actionDiff(nil, new[i], contextual); diff != nil {
diffs = append(diffs, diff)
}
}

sort.Sort(ObjectDiffs(diffs))

return diffs
}

func (t *TaskDiff) GoString() string {
var out string
if len(t.Annotations) == 0 {
Expand Down
Loading

0 comments on commit a0a56c9

Please sign in to comment.