feat(ansible): support with_together legacy loops
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
2655775a8f
commit
5bb3a2f636
6 changed files with 142 additions and 32 deletions
15
executor.go
15
executor.go
|
|
@ -661,8 +661,8 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin
|
|||
return coreerr.E("Executor.runTaskOnHost", sprintf("get client for %s", executionHost), err)
|
||||
}
|
||||
|
||||
// Handle loops, including legacy with_file, with_fileglob, and with_sequence syntax.
|
||||
if task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil {
|
||||
// Handle loops, including legacy with_file, with_fileglob, with_sequence, and with_together syntax.
|
||||
if task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil || task.WithTogether != nil {
|
||||
return e.runLoop(ctx, host, client, task, play, start)
|
||||
}
|
||||
|
||||
|
|
@ -864,6 +864,8 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC
|
|||
items, err = e.resolveWithFileGlobLoop(task.WithFileGlob, host, task)
|
||||
} else if task.WithSequence != nil {
|
||||
items, err = e.resolveWithSequenceLoop(task.WithSequence, host, task)
|
||||
} else if task.WithTogether != nil {
|
||||
items, err = e.resolveWithTogetherLoop(task.WithTogether, host, task)
|
||||
} else {
|
||||
items = e.resolveLoop(task.Loop, host)
|
||||
}
|
||||
|
|
@ -1141,6 +1143,15 @@ func (e *Executor) resolveWithSequenceLoop(loop any, host string, task *Task) ([
|
|||
return items, nil
|
||||
}
|
||||
|
||||
func (e *Executor) resolveWithTogetherLoop(loop any, host string, task *Task) ([]any, error) {
|
||||
items := expandTogetherLoop(loop)
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseSequenceSpec(loop any) (*sequenceSpec, error) {
|
||||
spec := &sequenceSpec{
|
||||
step: 1,
|
||||
|
|
|
|||
|
|
@ -919,6 +919,33 @@ func TestExecutor_RunTaskOnHost_Good_LoopFromWithNested(t *testing.T) {
|
|||
assert.Equal(t, "blue-large", result.Results[3].Msg)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHost_Good_LoopFromWithTogether(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.clients["host1"] = NewMockSSHClient()
|
||||
|
||||
task := &Task{
|
||||
Name: "Together loop",
|
||||
Module: "debug",
|
||||
Args: map[string]any{
|
||||
"msg": "{{ item.0 }}={{ item.1 }}",
|
||||
},
|
||||
WithTogether: []any{
|
||||
[]any{"red", "blue"},
|
||||
[]any{"small", "large", "medium"},
|
||||
},
|
||||
Register: "together_loop_result",
|
||||
}
|
||||
|
||||
err := e.runTaskOnHosts(context.Background(), []string{"host1"}, task, &Play{})
|
||||
require.NoError(t, err)
|
||||
|
||||
result := e.results["host1"]["together_loop_result"]
|
||||
require.NotNil(t, result)
|
||||
require.Len(t, result.Results, 2)
|
||||
assert.Equal(t, "red=small", result.Results[0].Msg)
|
||||
assert.Equal(t, "blue=large", result.Results[1].Msg)
|
||||
}
|
||||
|
||||
func TestExecutor_RunTaskOnHosts_Good_LoopNotifiesAndCallsCallback(t *testing.T) {
|
||||
e := NewExecutor("/tmp")
|
||||
e.clients["host1"] = NewMockSSHClient()
|
||||
|
|
|
|||
29
go.sum
29
go.sum
|
|
@ -4,57 +4,28 @@ dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
|||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg=
|
||||
forge.lthn.ai/core/go v0.3.0/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
|
|
|
|||
71
parser.go
71
parser.go
|
|
@ -357,7 +357,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
|||
"action": true, "local_action": true,
|
||||
"include_tasks": true, "import_tasks": true,
|
||||
"include_role": true, "import_role": true,
|
||||
"with_items": true, "with_dict": true, "with_indexed_items": true, "with_nested": true, "with_file": true, "with_fileglob": true, "with_sequence": true,
|
||||
"with_items": true, "with_dict": true, "with_indexed_items": true, "with_nested": true, "with_together": true, "with_file": true, "with_fileglob": true, "with_sequence": true,
|
||||
}
|
||||
|
||||
for key, val := range m {
|
||||
|
|
@ -462,6 +462,11 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
|||
t.WithSequence = sequence
|
||||
}
|
||||
|
||||
// Preserve with_together so the executor can zip legacy loop inputs at runtime.
|
||||
if t.WithTogether != nil && t.Loop == nil {
|
||||
t.Loop = expandTogetherLoop(t.WithTogether)
|
||||
}
|
||||
|
||||
// Support legacy action/local_action shorthands.
|
||||
if t.Module == "" {
|
||||
if localAction, ok := m["local_action"]; ok {
|
||||
|
|
@ -582,6 +587,35 @@ func expandNestedLoop(loop any) []any {
|
|||
return items
|
||||
}
|
||||
|
||||
// expandTogetherLoop converts with_together input into a zipped loop. Each
|
||||
// output item contains one value from each input group at the same index.
|
||||
func expandTogetherLoop(loop any) []any {
|
||||
groups, ok := togetherLoopGroups(loop)
|
||||
if !ok || len(groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
minLen := len(groups[0])
|
||||
for _, group := range groups[1:] {
|
||||
if len(group) < minLen {
|
||||
minLen = len(group)
|
||||
}
|
||||
}
|
||||
if minLen == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
items := make([]any, 0, minLen)
|
||||
for i := 0; i < minLen; i++ {
|
||||
combo := make([]any, len(groups))
|
||||
for j, group := range groups {
|
||||
combo[j] = group[i]
|
||||
}
|
||||
items = append(items, combo)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func nestedLoopGroups(loop any) ([][]any, bool) {
|
||||
switch v := loop.(type) {
|
||||
case []any:
|
||||
|
|
@ -638,6 +672,41 @@ func nestedLoopItems(value any) []any {
|
|||
}
|
||||
}
|
||||
|
||||
func togetherLoopGroups(loop any) ([][]any, bool) {
|
||||
switch v := loop.(type) {
|
||||
case []any:
|
||||
groups := make([][]any, 0, len(v))
|
||||
for _, group := range v {
|
||||
items := nestedLoopItems(group)
|
||||
if len(items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
groups = append(groups, items)
|
||||
}
|
||||
return groups, true
|
||||
case []string:
|
||||
items := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
items[i] = item
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return [][]any{items}, true
|
||||
case string:
|
||||
if v == "" {
|
||||
return nil, false
|
||||
}
|
||||
return [][]any{{v}}, true
|
||||
default:
|
||||
items := nestedLoopItems(v)
|
||||
if len(items) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return [][]any{items}, true
|
||||
}
|
||||
}
|
||||
|
||||
// isModule checks if a key is a known module.
|
||||
func isModule(key string) bool {
|
||||
for _, m := range KnownModules {
|
||||
|
|
|
|||
1
types.go
1
types.go
|
|
@ -121,6 +121,7 @@ type Task struct {
|
|||
WithFile any `yaml:"with_file,omitempty"`
|
||||
WithFileGlob any `yaml:"with_fileglob,omitempty"`
|
||||
WithSequence any `yaml:"with_sequence,omitempty"`
|
||||
WithTogether any `yaml:"with_together,omitempty"`
|
||||
IncludeRole *struct {
|
||||
Name string `yaml:"name"`
|
||||
TasksFrom string `yaml:"tasks_from,omitempty"`
|
||||
|
|
|
|||
|
|
@ -369,6 +369,37 @@ with_nested:
|
|||
assert.Equal(t, []any{"blue", "large"}, fourth)
|
||||
}
|
||||
|
||||
func TestTypes_Task_UnmarshalYAML_Good_WithTogether(t *testing.T) {
|
||||
input := `
|
||||
name: Together loop values
|
||||
debug:
|
||||
msg: "{{ item.0 }} {{ item.1 }}"
|
||||
with_together:
|
||||
- - red
|
||||
- blue
|
||||
- - small
|
||||
- large
|
||||
- medium
|
||||
`
|
||||
var task Task
|
||||
err := yaml.Unmarshal([]byte(input), &task)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, task.WithTogether)
|
||||
|
||||
items, ok := task.Loop.([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, items, 2)
|
||||
|
||||
first, ok := items[0].([]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []any{"red", "small"}, first)
|
||||
|
||||
second, ok := items[1].([]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []any{"blue", "large"}, second)
|
||||
}
|
||||
|
||||
func TestTypes_Task_UnmarshalYAML_Good_WithNotify(t *testing.T) {
|
||||
input := `
|
||||
name: Install package
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue