add result module (#2646)

Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
liujian 2025-06-30 13:42:18 +08:00 committed by GitHub
parent 794d28c706
commit bbb8a4a031
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 498 additions and 217 deletions

View File

@ -19,6 +19,7 @@ package v1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
const (
@ -43,6 +44,7 @@ const (
PlaybookPhaseSucceeded PlaybookPhase = "Succeeded"
)
// PlaybookFailedReason is the reason why a Playbook failed.
type PlaybookFailedReason string
const (
@ -54,7 +56,7 @@ const (
PlaybookFailedReasonTaskFailed PlaybookFailedReason = "task executor failed"
)
// PlaybookSpec of playbook.
// PlaybookSpec defines the desired state of Playbook.
type PlaybookSpec struct {
// Project is storage for executable packages
// +optional
@ -109,10 +111,12 @@ type PlaybookProject struct {
Token string `json:"token,omitempty"`
}
// PlaybookStatus of Playbook
// PlaybookStatus defines the observed state of Playbook.
type PlaybookStatus struct {
// TaskResult total related tasks execute result.
TaskResult PlaybookTaskResult `json:"taskResult,omitempty"`
// Statistics statistics of task counts
Statistics PlaybookStatistics `json:"statistics,omitempty"`
// Result will record the results detail.
Result runtime.RawExtension `json:"result,omitempty"`
// Phase of playbook.
Phase PlaybookPhase `json:"phase,omitempty"`
// FailureReason will be set in the event that there is a terminal problem
@ -121,40 +125,20 @@ type PlaybookStatus struct {
// FailureMessage will be set in the event that there is a terminal problem
// +optional
FailureMessage string `json:"failureMessage,omitempty"`
// FailedDetail will record the failed tasks.
FailedDetail []PlaybookFailedDetail `json:"failedDetail,omitempty"`
}
// PlaybookTaskResult of Playbook
type PlaybookTaskResult struct {
// Total number of tasks.
// PlaybookStatistics contains statistics of task counts.
type PlaybookStatistics struct {
// Total number of tasks
Total int `json:"total,omitempty"`
// Success number of tasks.
// Number of successful tasks
Success int `json:"success,omitempty"`
// Failed number of tasks.
// Number of failed tasks
Failed int `json:"failed,omitempty"`
// Ignored number of tasks.
// Number of ignored tasks
Ignored int `json:"ignored,omitempty"`
}
// PlaybookFailedDetail store failed message when playbook run failed.
type PlaybookFailedDetail struct {
// Task name of failed task.
Task string `json:"task,omitempty"`
// failed Hosts Result of failed task.
Hosts []PlaybookFailedDetailHost `json:"hosts,omitempty"`
}
// PlaybookFailedDetailHost detail failed message for each host.
type PlaybookFailedDetailHost struct {
// Host name of failed task.
Host string `json:"host,omitempty"`
// Stdout of failed task.
Stdout string `json:"stdout,omitempty"`
// StdErr of failed task.
StdErr string `json:"stdErr,omitempty"`
}
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:openapi-gen=true
@ -165,6 +149,7 @@ type PlaybookFailedDetailHost struct {
// +kubebuilder:printcolumn:name="Total",type="integer",JSONPath=".status.taskResult.total"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// Playbook is the Schema for the playbooks API.
// Playbook resource executor a playbook.
type Playbook struct {
metav1.TypeMeta `json:",inline"`
@ -176,13 +161,14 @@ type Playbook struct {
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// PlaybookList of Playbook
// PlaybookList contains a list of Playbook.
type PlaybookList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Playbook `json:"items"`
}
// Register Playbook and PlaybookList types with the scheme.
func init() {
SchemeBuilder.Register(&Playbook{}, &PlaybookList{})
}

View File

@ -220,41 +220,6 @@ func (in *Playbook) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaybookFailedDetail) DeepCopyInto(out *PlaybookFailedDetail) {
*out = *in
if in.Hosts != nil {
in, out := &in.Hosts, &out.Hosts
*out = make([]PlaybookFailedDetailHost, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaybookFailedDetail.
func (in *PlaybookFailedDetail) DeepCopy() *PlaybookFailedDetail {
if in == nil {
return nil
}
out := new(PlaybookFailedDetail)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaybookFailedDetailHost) DeepCopyInto(out *PlaybookFailedDetailHost) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaybookFailedDetailHost.
func (in *PlaybookFailedDetailHost) DeepCopy() *PlaybookFailedDetailHost {
if in == nil {
return nil
}
out := new(PlaybookFailedDetailHost)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaybookList) DeepCopyInto(out *PlaybookList) {
*out = *in
@ -348,17 +313,26 @@ func (in *PlaybookSpec) DeepCopy() *PlaybookSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaybookStatistics) DeepCopyInto(out *PlaybookStatistics) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaybookStatistics.
func (in *PlaybookStatistics) DeepCopy() *PlaybookStatistics {
if in == nil {
return nil
}
out := new(PlaybookStatistics)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaybookStatus) DeepCopyInto(out *PlaybookStatus) {
*out = *in
out.TaskResult = in.TaskResult
if in.FailedDetail != nil {
in, out := &in.FailedDetail, &out.FailedDetail
*out = make([]PlaybookFailedDetail, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.Statistics = in.Statistics
in.Result.DeepCopyInto(&out.Result)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaybookStatus.
@ -370,18 +344,3 @@ func (in *PlaybookStatus) DeepCopy() *PlaybookStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlaybookTaskResult) DeepCopyInto(out *PlaybookTaskResult) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlaybookTaskResult.
func (in *PlaybookTaskResult) DeepCopy() *PlaybookTaskResult {
if in == nil {
return nil
}
out := new(PlaybookTaskResult)
in.DeepCopyInto(out)
return out
}

View File

@ -30,7 +30,9 @@ spec:
name: v1
schema:
openAPIV3Schema:
description: Playbook resource executor a playbook.
description: |-
Playbook is the Schema for the playbooks API.
Playbook resource executor a playbook.
properties:
apiVersion:
description: |-
@ -50,7 +52,7 @@ spec:
metadata:
type: object
spec:
description: PlaybookSpec of playbook.
description: PlaybookSpec defines the desired state of Playbook.
properties:
config:
description: Config is the global variable configuration for playbook
@ -74,11 +76,6 @@ spec:
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
debug:
description: |-
If Debug mode is true, It will retain runtime data after a successful execution of Playbook,
which includes task execution status and parameters.
type: boolean
inventoryRef:
description: InventoryRef is the node configuration for playbook
properties:
@ -1996,36 +1993,8 @@ spec:
- playbook
type: object
status:
description: PlaybookStatus of Playbook
description: PlaybookStatus defines the observed state of Playbook.
properties:
failedDetail:
description: FailedDetail will record the failed tasks.
items:
description: PlaybookFailedDetail store failed message when playbook
run failed.
properties:
hosts:
description: failed Hosts Result of failed task.
items:
description: PlaybookFailedDetailHost detail failed message
for each host.
properties:
host:
description: Host name of failed task.
type: string
stdErr:
description: StdErr of failed task.
type: string
stdout:
description: Stdout of failed task.
type: string
type: object
type: array
task:
description: Task name of failed task.
type: string
type: object
type: array
failureMessage:
description: FailureMessage will be set in the event that there is
a terminal problem
@ -2037,20 +2006,24 @@ spec:
phase:
description: Phase of playbook.
type: string
taskResult:
description: TaskResult total related tasks execute result.
result:
description: Result will record the results detail.
type: object
x-kubernetes-preserve-unknown-fields: true
statistics:
description: Statistics statistics of task counts
properties:
failed:
description: Failed number of tasks.
description: Number of failed tasks
type: integer
ignored:
description: Ignored number of tasks.
description: Number of ignored tasks
type: integer
success:
description: Success number of tasks.
description: Number of successful tasks
type: integer
total:
description: Total number of tasks.
description: Total number of tasks
type: integer
type: object
type: object

View File

@ -30,7 +30,9 @@ spec:
name: v1
schema:
openAPIV3Schema:
description: Playbook resource executor a playbook.
description: |-
Playbook is the Schema for the playbooks API.
Playbook resource executor a playbook.
properties:
apiVersion:
description: |-
@ -50,7 +52,7 @@ spec:
metadata:
type: object
spec:
description: PlaybookSpec of playbook.
description: PlaybookSpec defines the desired state of Playbook.
properties:
config:
description: Config is the global variable configuration for playbook
@ -74,11 +76,6 @@ spec:
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
debug:
description: |-
If Debug mode is true, It will retain runtime data after a successful execution of Playbook,
which includes task execution status and parameters.
type: boolean
inventoryRef:
description: InventoryRef is the node configuration for playbook
properties:
@ -1996,36 +1993,8 @@ spec:
- playbook
type: object
status:
description: PlaybookStatus of Playbook
description: PlaybookStatus defines the observed state of Playbook.
properties:
failedDetail:
description: FailedDetail will record the failed tasks.
items:
description: PlaybookFailedDetail store failed message when playbook
run failed.
properties:
hosts:
description: failed Hosts Result of failed task.
items:
description: PlaybookFailedDetailHost detail failed message
for each host.
properties:
host:
description: Host name of failed task.
type: string
stdErr:
description: StdErr of failed task.
type: string
stdout:
description: Stdout of failed task.
type: string
type: object
type: array
task:
description: Task name of failed task.
type: string
type: object
type: array
failureMessage:
description: FailureMessage will be set in the event that there is
a terminal problem
@ -2037,20 +2006,24 @@ spec:
phase:
description: Phase of playbook.
type: string
taskResult:
description: TaskResult total related tasks execute result.
result:
description: Result will record the results detail.
type: object
x-kubernetes-preserve-unknown-fields: true
statistics:
description: Statistics statistics of task counts
properties:
failed:
description: Failed number of tasks.
description: Number of failed tasks
type: integer
ignored:
description: Ignored number of tasks.
description: Number of ignored tasks
type: integer
success:
description: Success number of tasks.
description: Number of successful tasks
type: integer
total:
description: Total number of tasks.
description: Total number of tasks
type: integer
type: object
type: object

View File

@ -64,6 +64,7 @@ task执行时, 会在定义的host分别上执行.
- [gen_cert](modules/gen_cert.md)
- [image](modules/image.md)
- [prometheus](modules/prometheus.md)
- [result](modules/result.md)
- [set_fact](modules/set_fact.md)
- [setup](modules/setup.md)
- [template](modules/template.md)

73
docs/zh/modules/result.md Normal file
View File

@ -0,0 +1,73 @@
# result 模块
result模块允许用户将变量设置到playbook的status detail中显示。
## 参数
| 参数 | 说明 | 类型 | 必填 | 默认值 |
|------|------|------|------|-------|
| any | 需要设置的任意参数 | 字符串或map | 否 | - |
## 使用示例
1. 设置字符串参数
```yaml
- name: set string
result:
a: b
c: d
```
playbook中status显示为
```yaml
apiVersion: kubekey.kubesphere.io/v1
kind: Playbook
status:
detail:
a: b
c: d
phase: Succeeded
```
2. 设置map参数
```yaml
- name: set map
result:
a:
b: c
```
playbook中status显示为
```yaml
apiVersion: kubekey.kubesphere.io/v1
kind: Playbook
status:
detail:
a:
b: c
phase: Succeeded
```
3. 设置多个result
```yaml
- name: set result1
result:
k1: v1
- name: set result2
result:
k2: v2
- name: set result3
result:
k2: v3
```
所有的结果都会合并如果有重复的key值以最后设置的key为准。
playbook中status显示为
```yaml
apiVersion: kubekey.kubesphere.io/v1
kind: Playbook
status:
detail:
k1: v1
k2: v3
phase: Succeeded
```

View File

@ -18,6 +18,7 @@ package executor
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
@ -130,7 +131,17 @@ func (e playbookExecutor) syncStatus(ctx context.Context, err error) {
}
fmt.Fprintf(e.logOutput, "%s [Playbook %s] finish. total: %v,success: %v,ignored: %v,failed: %v\n", time.Now().Format(time.TimeOnly+" MST"), ctrlclient.ObjectKeyFromObject(e.playbook),
e.playbook.Status.TaskResult.Total, e.playbook.Status.TaskResult.Success, e.playbook.Status.TaskResult.Ignored, e.playbook.Status.TaskResult.Failed)
e.playbook.Status.Statistics.Total, e.playbook.Status.Statistics.Success, e.playbook.Status.Statistics.Ignored, e.playbook.Status.Statistics.Failed)
// fill results from variable
rv, err := e.variable.Get(variable.GetResultVariable())
if err != nil {
klog.ErrorS(err, "failed to get playbook results detail", "playbook", ctrlclient.ObjectKeyFromObject(e.playbook))
}
e.playbook.Status.Result.Raw, err = json.Marshal(rv)
if err != nil {
klog.ErrorS(err, "failed to marshal playbook results detail", "playbook", ctrlclient.ObjectKeyFromObject(e.playbook))
}
// update playbook status
if err := e.client.Status().Patch(ctx, e.playbook, ctrlclient.MergeFrom(cp)); err != nil {

View File

@ -9,7 +9,6 @@ import (
"time"
"github.com/cockroachdb/errors"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
"github.com/schollz/progressbar/v3"
"gopkg.in/yaml.v3"
@ -43,14 +42,14 @@ func (e *taskExecutor) Exec(ctx context.Context) error {
return errors.Wrapf(err, "failed to create task %q", e.task.Spec.Name)
}
defer func() {
e.playbook.Status.TaskResult.Total++
e.playbook.Status.Statistics.Total++
switch e.task.Status.Phase {
case kkcorev1alpha1.TaskPhaseSuccess:
e.playbook.Status.TaskResult.Success++
e.playbook.Status.Statistics.Success++
case kkcorev1alpha1.TaskPhaseIgnored:
e.playbook.Status.TaskResult.Ignored++
e.playbook.Status.Statistics.Ignored++
case kkcorev1alpha1.TaskPhaseFailed:
e.playbook.Status.TaskResult.Failed++
e.playbook.Status.Statistics.Failed++
}
}()
// run task
@ -59,20 +58,6 @@ func (e *taskExecutor) Exec(ctx context.Context) error {
}
// exit when task run failed
if e.task.IsFailed() {
var hostReason []kkcorev1.PlaybookFailedDetailHost
for _, tr := range e.task.Status.HostResults {
hostReason = append(hostReason, kkcorev1.PlaybookFailedDetailHost{
Host: tr.Host,
Stdout: tr.Stdout,
StdErr: tr.StdErr,
})
}
e.playbook.Status.FailedDetail = append(e.playbook.Status.FailedDetail, kkcorev1.PlaybookFailedDetail{
Task: e.task.Spec.Name,
Hosts: hostReason,
})
e.playbook.Status.Phase = kkcorev1.PlaybookPhaseFailed
return errors.Errorf("task %q run failed", e.task.Spec.Name)
}

65
pkg/modules/result.go Normal file
View File

@ -0,0 +1,65 @@
package modules
import (
"context"
"fmt"
"gopkg.in/yaml.v3"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
/*
The Result module allows setting result variables during playbook execution.
This module enables users to define and update result variables that can be accessed
by subsequent tasks in the same playbook.
Configuration:
Users can specify key-value pairs to set as result variables:
result:
key1: value1 # required: result variable name and value
key2: value2 # optional: additional result variables
Usage Examples in Playbook Tasks:
1. Set single result variable:
```yaml
- name: Set result variable
result:
app_version: "1.0.0"
register: version_result
```
2. Set multiple result variables:
```yaml
- name: Set result configuration variables
result:
db_host: "localhost"
db_port: 5432
register: config_vars
```
Return Values:
- On success: Returns "Success" in stdout
- On failure: Returns error message in stderr
*/
// ModuleResult handles the "result" module, setting result variables during playbook execution
func ModuleResult(ctx context.Context, options ExecOptions) (string, string) {
var node yaml.Node
// Unmarshal the YAML document into a root node.
if err := yaml.Unmarshal(options.Args.Raw, &node); err != nil {
return "", fmt.Sprintf("failed to unmarshal YAML error: %v", err)
}
if err := options.Variable.Merge(variable.MergeResultVariable(node, options.Host)); err != nil {
return "", fmt.Sprintf("result error: %v", err)
}
return StdoutSuccess, ""
}
func init() {
utilruntime.Must(RegisterModule("result", ModuleResult))
}

115
pkg/modules/result_test.go Normal file
View File

@ -0,0 +1,115 @@
/*
Copyright 2023 The KubeSphere Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package modules
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
)
func TestResult(t *testing.T) {
testcases := []struct {
name string
opt ExecOptions
exceptStdout string
exceptStderr string
}{
{
name: "string value",
opt: ExecOptions{
Args: runtime.RawExtension{
Raw: []byte(`{"k": "v"}`),
},
Host: "",
Variable: &testVariable{},
Task: kkcorev1alpha1.Task{},
Playbook: kkcorev1.Playbook{},
},
exceptStdout: "success",
},
{
name: "int value",
opt: ExecOptions{
Args: runtime.RawExtension{
Raw: []byte(`{"k": 1}`),
},
Host: "",
Variable: &testVariable{},
Task: kkcorev1alpha1.Task{},
Playbook: kkcorev1.Playbook{},
},
exceptStdout: "success",
},
{
name: "float value",
opt: ExecOptions{
Args: runtime.RawExtension{
Raw: []byte(`{"k": 1.1}`),
},
Host: "",
Variable: &testVariable{},
Task: kkcorev1alpha1.Task{},
Playbook: kkcorev1.Playbook{},
},
exceptStdout: "success",
},
{
name: "map value",
opt: ExecOptions{
Args: runtime.RawExtension{
Raw: []byte(`{"k": {"k1": "v1", "k2": "v2"}}`),
},
Host: "",
Variable: &testVariable{},
Task: kkcorev1alpha1.Task{},
Playbook: kkcorev1.Playbook{},
},
exceptStdout: "success",
},
{
name: "array value",
opt: ExecOptions{
Args: runtime.RawExtension{
Raw: []byte(`{"k": ["v1", "v2"]}`),
},
Host: "",
Variable: &testVariable{},
Task: kkcorev1alpha1.Task{},
Playbook: kkcorev1.Playbook{},
},
exceptStdout: "success",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
stdout, stderr := ModuleResult(ctx, tc.opt)
assert.Equal(t, tc.exceptStdout, stdout)
assert.Equal(t, tc.exceptStderr, stderr)
})
}
}

View File

@ -145,6 +145,8 @@ type value struct {
Inventory kkcorev1.Inventory
// Hosts store the variable for running tasks on specific hosts
Hosts map[string]host
// result store the variable which set by result task.
Result map[string]any
}
type host struct {
@ -163,6 +165,7 @@ func (v *variable) DeepCopy() *variable {
Config: *v.value.Config.DeepCopy(),
Inventory: *v.value.Inventory.DeepCopy(),
Hosts: make(map[string]host, len(v.value.Hosts)),
Result: maps.Clone(v.value.Result),
}
for k, h := range v.value.Hosts {
copyVal.Hosts[k] = host{
@ -196,12 +199,12 @@ func (v *variable) Merge(f MergeFunc) error {
if err := f(nv); err != nil {
return err
}
return v.syncSource(*nv.value)
}
// syncSource sync hosts vars to source.
func (v *variable) syncSource(newVal value) error {
v.value.Result = newVal.Result
for hn, hv := range v.value.Hosts {
if reflect.DeepEqual(newVal.Hosts[hn], hv) {
// nothing change skip.

View File

@ -17,7 +17,10 @@ import (
// ***************************** GetFunc ***************************** //
// GetHostnames get all hostnames from a group or host
// GetHostnames retrieves all hostnames from specified groups or hosts.
// It supports various hostname patterns including direct hostnames, group names,
// indexed group access (e.g., "group[0]"), and random selection (e.g., "group|random").
// The function also supports template parsing for hostnames using configuration variables.
var GetHostnames = func(name []string) GetFunc {
if len(name) == 0 {
return emptyGetFunc
@ -30,24 +33,23 @@ var GetHostnames = func(name []string) GetFunc {
}
var hs []string
for _, n := range name {
// try parse hostname by Config.
// Try to parse hostname using configuration variables as template context
if pn, err := tmpl.ParseFunc(Extension2Variables(vv.value.Config.Spec), n, func(b []byte) string { return string(b) }); err == nil {
n = pn
}
// add host to hs
// Add direct hostname if it exists in the hosts map
if _, exists := vv.value.Hosts[n]; exists {
hs = append(hs, n)
}
// add group's host to gs
// Add all hosts from matching groups
for gn, gv := range ConvertGroup(vv.value.Inventory) {
if gn == n {
hs = CombineSlice(hs, gv)
break
}
}
// Add the specified host in the specified group to the hs.
// Handle indexed group access (e.g., "group[0]")
regexForIndex := regexp.MustCompile(`^(.*?)\[(\d+)\]$`)
if match := regexForIndex.FindStringSubmatch(strings.TrimSpace(n)); match != nil {
index, err := strconv.Atoi(match[2])
@ -62,7 +64,7 @@ var GetHostnames = func(name []string) GetFunc {
}
}
// add random host in group
// Handle random host selection from group (e.g., "group|random")
regexForRandom := regexp.MustCompile(`^(.+?)\s*\|\s*random$`)
if match := regexForRandom.FindStringSubmatch(strings.TrimSpace(n)); match != nil {
if group, ok := ConvertGroup(vv.value.Inventory)[match[1]]; ok {
@ -75,9 +77,11 @@ var GetHostnames = func(name []string) GetFunc {
}
}
// GetAllVariable get all variable for a given host
// GetAllVariable retrieves all variables for a given host, including group variables,
// remote variables, runtime variables, inventory variables, and configuration variables.
// It also sets default variables for localhost and provides access to global host and group information.
var GetAllVariable = func(hostname string) GetFunc {
// getLocalIP get the ipv4 or ipv6 for localhost machine
// getLocalIP retrieves the IPv4 or IPv6 address for the localhost machine
getLocalIP := func(ipType string) string {
addrs, err := net.InterfaceAddrs()
if err != nil {
@ -101,7 +105,8 @@ var GetAllVariable = func(hostname string) GetFunc {
return ""
}
// defaultHostVariable set default vars when hostname is "localhost"
// defaultHostVariable sets default variables when hostname is "localhost"
// It automatically detects and sets IPv4/IPv6 addresses and hostname information
defaultHostVariable := func(hostname string, hostVars map[string]any) {
if hostname == _const.VariableLocalHost {
if _, ok := hostVars[_const.VariableIPv4]; !ok {
@ -112,7 +117,7 @@ var GetAllVariable = func(hostname string) GetFunc {
}
}
if os, ok := hostVars[_const.VariableOS]; ok {
// try to set hostname by current actual hostname.
// Try to set hostname by current actual hostname from OS information
if osd, ok := os.(map[string]any); ok {
hostVars[_const.VariableHostName] = osd[_const.VariableOSHostName]
}
@ -125,28 +130,30 @@ var GetAllVariable = func(hostname string) GetFunc {
}
}
// getHostsVariable builds a complete variable map for all hosts
// by combining variables from multiple sources in a specific order
getHostsVariable := func(v *variable) map[string]any {
globalHosts := make(map[string]any)
for hostname := range v.value.Hosts {
hostVars := make(map[string]any)
// set groups vars
// Set group variables for hosts that belong to groups
for _, gv := range v.value.Inventory.Spec.Groups {
if slices.Contains(gv.Hosts, hostname) {
hostVars = CombineVariables(hostVars, Extension2Variables(gv.Vars))
}
}
// find from remote
// Merge remote variables (variables collected from the actual host)
hostVars = CombineVariables(hostVars, v.value.Hosts[hostname].RemoteVars)
// merge from runtime
// Merge runtime variables (variables set during playbook execution)
hostVars = CombineVariables(hostVars, v.value.Hosts[hostname].RuntimeVars)
// merge from inventory vars
// Merge inventory-level variables
hostVars = CombineVariables(hostVars, Extension2Variables(v.value.Inventory.Spec.Vars))
// merge from inventory host vars
// Merge host-specific variables from inventory
hostVars = CombineVariables(hostVars, Extension2Variables(v.value.Inventory.Spec.Hosts[hostname]))
// merge from config
// Merge configuration variables
hostVars = CombineVariables(hostVars, Extension2Variables(v.value.Config.Spec))
// set default localhost
// Set default variables for localhost
defaultHostVariable(hostname, hostVars)
globalHosts[hostname] = hostVars
}
@ -162,12 +169,14 @@ var GetAllVariable = func(hostname string) GetFunc {
hosts := getHostsVariable(vv)
hostVars, ok := hosts[hostname].(map[string]any)
if !ok {
// cannot found hosts variable.
// Return empty map if host variables cannot be found
return make(map[string]any), nil
}
// Add global hosts information to the host variables
hostVars = CombineVariables(hostVars, map[string]any{
_const.VariableGlobalHosts: hosts,
})
// Add group information to the host variables
hostVars = CombineVariables(hostVars, map[string]any{
_const.VariableGroups: ConvertGroup(vv.value.Inventory),
})
@ -176,7 +185,8 @@ var GetAllVariable = func(hostname string) GetFunc {
}
}
// GetHostMaxLength get the max length for all hosts
// GetHostMaxLength calculates the maximum length of all hostnames.
// This is useful for formatting output or determining display widths.
var GetHostMaxLength = func() GetFunc {
return func(v Variable) (any, error) {
vv, ok := v.(*variable)
@ -203,3 +213,16 @@ var GetWorkDir = func() GetFunc {
return _const.GetWorkdirFromConfig(vv.value.Config), nil
}
}
// GetResultVariable returns the global result variables.
// This function retrieves the result variables that are set globally and accessible across all hosts.
var GetResultVariable = func() GetFunc {
return func(v Variable) (any, error) {
vv, ok := v.(*variable)
if !ok {
return nil, errors.New("variable type error")
}
return vv.value.Result, nil
}
}

View File

@ -7,7 +7,9 @@ import (
// ***************************** MergeFunc ***************************** //
// MergeRemoteVariable merge variable to remote.
// MergeRemoteVariable merges remote variables to a specific host.
// It takes a map of data and a hostname, and merges the data into the host's RemoteVars
// if the RemoteVars are currently empty. This prevents overwriting existing remote variables.
var MergeRemoteVariable = func(data map[string]any, hostname string) MergeFunc {
return func(v Variable) error {
vv, ok := v.(*variable)
@ -32,7 +34,11 @@ var MergeRemoteVariable = func(data map[string]any, hostname string) MergeFunc {
}
}
// MergeRuntimeVariable parse variable by specific host and merge to the host.
// MergeRuntimeVariable parses variables using a specific host's context and merges them to the host's runtime variables.
// It takes a YAML node and a list of hostnames, then for each host:
// 1. Gets all variables for the host to create a parsing context
// 2. Parses the YAML node using that context
// 3. Merges the parsed data into the host's RuntimeVars
var MergeRuntimeVariable = func(node yaml.Node, hosts ...string) MergeFunc {
if node.IsZero() {
// skip
@ -68,7 +74,10 @@ var MergeRuntimeVariable = func(node yaml.Node, hosts ...string) MergeFunc {
}
}
// MergeHostsRuntimeVariable parse variable by specific host and merge to given hosts.
// MergeHostsRuntimeVariable parses variables using a specific host's context and merges them to multiple hosts' runtime variables.
// It takes a YAML node, a source hostname for context, and a list of target hostnames.
// The function uses the source host's variables as context to parse the YAML node,
// then merges the parsed data into each target host's RuntimeVars.
var MergeHostsRuntimeVariable = func(node yaml.Node, hostname string, hosts ...string) MergeFunc {
if node.IsZero() {
// skip
@ -103,3 +112,39 @@ var MergeHostsRuntimeVariable = func(node yaml.Node, hostname string, hosts ...s
return nil
}
}
// MergeResultVariable parses variables using a specific host's context and sets them as global result variables.
// It takes a YAML node and a hostname, then:
// 1. Gets all variables for the host to create a parsing context
// 2. Parses the YAML node using that context
// 3. Sets the parsed data as the global result variables (accessible across all hosts)
var MergeResultVariable = func(node yaml.Node, hostname string) MergeFunc {
if node.IsZero() {
// skip
return emptyMergeFunc
}
return func(v Variable) error {
vv, ok := v.(*variable)
if !ok {
return errors.New("variable type error")
}
// Avoid nested locking: prepare context for parsing outside locking region
curVars, err := v.Get(GetAllVariable(hostname))
if err != nil {
return err
}
ctx, ok := curVars.(map[string]any)
if !ok {
return errors.Errorf("host %s variables type error, expect map[string]any", hostname)
}
result, err := parseYamlNode(ctx, node)
if err != nil {
return err
}
vv.value.Result = CombineVariables(vv.value.Result, result)
return nil
}
}

View File

@ -107,3 +107,72 @@ func TestMergeRuntimeVariable(t *testing.T) {
})
}
}
func TestMergeResultVariable(t *testing.T) {
testcases := []struct {
name string
host string
variable *variable
data map[string]any
except value
}{
{
name: "success",
host: "n1",
variable: &variable{
source: source.NewMemorySource(),
value: &value{
Hosts: map[string]host{
"n1": {
RuntimeVars: map[string]any{
"k1": "v1",
},
},
"n2": {
RuntimeVars: map[string]any{
"k1": "v2",
},
},
},
},
},
data: map[string]any{
"v1": "{{ .k1 }}",
"v2": "vv",
},
except: value{
Hosts: map[string]host{
"n1": {
RuntimeVars: map[string]any{
"k1": "v1",
},
},
"n2": {
RuntimeVars: map[string]any{
"k1": "v2",
},
},
},
Result: map[string]any{
"v1": "v1",
"v2": "vv",
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
node, err := converter.ConvertMap2Node(tc.data)
if err != nil {
t.Fatal(err)
}
if err = tc.variable.Merge(MergeResultVariable(node, tc.host)); err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.except, *tc.variable.value)
})
}
}