Skip to content

Commit

Permalink
Merge pull request #148 from hashicorp/waypoint/agents/load-all-vars-…
Browse files Browse the repository at this point in the history
…for-execution

Waypoint Agent: Update execute to parse all waypoint variables
  • Loading branch information
briancain authored Aug 22, 2024
2 parents 237cde9 + af4f15f commit e2e1513
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 29 deletions.
3 changes: 3 additions & 0 deletions .changelog/148.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
Support loading all variables from Waypoint server in Waypoint agent CLI
```
21 changes: 14 additions & 7 deletions internal/commands/waypoint/agent/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ func NewCmdRun(ctx *cmd.Context) *cmd.Command {
var agentRunDuration = 60 * time.Second

func agentRun(log hclog.Logger, opts *RunOpts) error {
// Only set level to info if not in debug mode
if !log.IsDebug() {
// Give log.Info level feedback to user when they run the agent CLI
log.SetLevel(hclog.Info)
}

cfg, err := agent.ParseConfigFile(opts.ConfigPath)
if err != nil {
return err
Expand Down Expand Up @@ -147,14 +153,15 @@ func runOp(
exec *agent.Executor,
ns string,
) {

var (
status string
statusCode int
)

log = log.With("group", ao.Group, "operation", ao.ID, "action-run-id", ao.ActionRunID)

if ao.ActionRunID != "" {
log.Info("reporting action run starting", "action-run-id", ao.ActionRunID)
log.Info("reporting action run starting")

resp, err := opts.WS.WaypointServiceStartingAction(&waypoint_service.WaypointServiceStartingActionParams{
NamespaceID: ns,
Expand All @@ -169,7 +176,7 @@ func runOp(
log.Error("unable to register action as starting", "error", err)
} else {
defer func() {
log.Info("reporting action run ended", "id", resp.Payload.ActionRunID, "status", status, "status-code", statusCode)
log.Info("reporting action run ended", "status", status, "status-code", statusCode)

_, err = opts.WS.WaypointServiceEndingAction(&waypoint_service.WaypointServiceEndingActionParams{
NamespaceID: ns,
Expand All @@ -193,28 +200,28 @@ func runOp(
status = "internal error: " + err.Error()
statusCode = 1

log.Error("error resolving operation", "group", ao.Group, "operation", ao.ID, "error", err)
log.Error("error resolving operation", "error", err)
return
}

if !ok {
status = "unknown operation: " + ao.ID
statusCode = 127

log.Error("requested unknown operation", "id", ao.ID)
log.Error("requested unknown operation", "status", status, "status-code", statusCode)
return
}

opStat, err := exec.Execute(ctx, ao)
if err != nil {
status = "error execution operation: " + err.Error()

log.Error("error executing operation", "group", ao.Group, "operation", ao.ID, "error", err)
log.Error("error executing operation", "error", err)
return
}

status = opStat.Status
statusCode = opStat.Code

log.Info("finished operation", "id", ao.ID, "status", status, "status-code", statusCode)
log.Info("finished operation")
}
100 changes: 80 additions & 20 deletions internal/pkg/waypoint/agent/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package agent
import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -42,39 +44,39 @@ func (e *Executor) IsAvailable(opInfo *models.HashicorpCloudWaypointAgentOperati
}

func (e *Executor) Execute(ctx context.Context, opInfo *models.HashicorpCloudWaypointAgentOperation) (OperationStatus, error) {
var hctx hcl.EvalContext

input := make(map[string]cty.Value)
var (
hctx hcl.EvalContext
varMap map[string]any
)

if len(opInfo.Body) != 0 {

var rawInput map[string]any

err := json.Unmarshal(opInfo.Body, &rawInput)
if err != nil {
return errStatus, err
}

for k, v := range rawInput {
switch sv := v.(type) {
case float64:
input[k] = cty.NumberFloatVal(sv)
case string:
input[k] = cty.StringVal(sv)
case bool:
input[k] = cty.BoolVal(sv)
default:
// TODO how should we deal with these?
}
varMap, err = buildVariableMap(rawInput)
if err != nil {
return errStatus, err
}
} else {
varMap = make(map[string]any)
}

hctx.Variables = map[string]cty.Value{
"waypoint": cty.ObjectVal(map[string]cty.Value{
"run_id": cty.StringVal(opInfo.ActionRunID),
}),
"var": cty.ObjectVal(input),
// NOTE(briancain): This might overwrite any waypoint varibles if we ever
// decided to set those on the server. Additionally, it might make more sense
// to include this `waypoint.run_id` as a variable set by the server in the
// future.
varMap["waypoint"] = map[string]any{
"run_id": opInfo.ActionRunID,
}
_, ctyMap := anyToCty(varMap)

// Set all variables from the server on the HCL context so we can parse the
// agent config if any interpolated variables are defined
hctx.Variables = ctyMap

op, err := e.Config.Action(opInfo.Group, opInfo.ID, &hctx)
if err != nil {
Expand All @@ -83,3 +85,61 @@ func (e *Executor) Execute(ctx context.Context, opInfo *models.HashicorpCloudWay

return op.Run(ctx, e.Log)
}

// buildVariableMap takes a map of string any values where the keys are expected
// to be dot separated and builds a nested map of the values. This format can
// then be used by anyToCty to walk the map structure and build a map of cty
// values that HCL understands and can use to parse a config.
func buildVariableMap(rawInput map[string]any) (map[string]any, error) {
ret := make(map[string]any)

for k, v := range rawInput {
parts := strings.Split(k, ".")
cur := ret

for _, p := range parts[:len(parts)-1] {
if curVal, ok := cur[p]; ok {
curMap, ok := curVal.(map[string]any)
if ok {
cur = curMap
} else {
return nil, errors.Errorf("invalid input key %s %v", k, curVal)
}
} else {
curMap := make(map[string]any)
cur[p] = curMap
cur = curMap
}
}

cur[parts[len(parts)-1]] = v
}

return ret, nil
}

// anyToCty takes a map of string any values and converts them to cty values
// that HCL understands. This function will walk the map and convert the values
// to cty values that HCL can use to parse a config.
func anyToCty(objMap map[string]any) (cty.Value, map[string]cty.Value) {
obj := make(map[string]cty.Value)

for k, v := range objMap {
switch sv := v.(type) {
case map[string]any:
// Recuse and walk the map for its children
obj[k], _ = anyToCty(sv)
case float64:
obj[k] = cty.NumberFloatVal(sv)
case bool:
obj[k] = cty.BoolVal(sv)
case string:
obj[k] = cty.StringVal(sv)
default:
// Unhandled var type
obj[k] = cty.StringVal(fmt.Sprintf("%v", v))
}
}

return cty.ObjectVal(obj), obj
}
12 changes: 10 additions & 2 deletions internal/pkg/waypoint/agent/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,16 @@ func TestExecutor(t *testing.T) {
e.Config = cfg

data, err := json.Marshal(map[string]any{
"type": "nerf",
"var.type": "nerf",
"action.name": "launch",
"application.templateName": "cool-test-template", // pretend its in an app
"application.name": "test",
"application.outputs.run_id": "1234",
"application.inputs.region": "us-west-1",
"addon.abc123.outputs.database_url": "http://localhost:5432",
"addon.xyz098.outputs.load_balancer_ip": "http://localhost:8080",
"var.local_variable": "local-value",
"var.token": "token",
})
r.NoError(err)

Expand All @@ -124,7 +133,6 @@ func TestExecutor(t *testing.T) {
ID: "launch",
Body: data,
})

r.NoError(err)
})

Expand Down

0 comments on commit e2e1513

Please sign in to comment.