diff --git a/api/core/v1/playbook_types.go b/api/core/v1/playbook_types.go index d8570774..d3ba2f89 100644 --- a/api/core/v1/playbook_types.go +++ b/api/core/v1/playbook_types.go @@ -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{}) } diff --git a/api/core/v1/zz_generated.deepcopy.go b/api/core/v1/zz_generated.deepcopy.go index 48d6a2fc..9820019f 100644 --- a/api/core/v1/zz_generated.deepcopy.go +++ b/api/core/v1/zz_generated.deepcopy.go @@ -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 -} diff --git a/config/capkk/crds/kubekey.kubesphere.io_playbooks.yaml b/config/capkk/crds/kubekey.kubesphere.io_playbooks.yaml index 239e2b84..5521c9bc 100644 --- a/config/capkk/crds/kubekey.kubesphere.io_playbooks.yaml +++ b/config/capkk/crds/kubekey.kubesphere.io_playbooks.yaml @@ -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 diff --git a/config/kubekey/crds/kubekey.kubesphere.io_playbooks.yaml b/config/kubekey/crds/kubekey.kubesphere.io_playbooks.yaml index 239e2b84..5521c9bc 100644 --- a/config/kubekey/crds/kubekey.kubesphere.io_playbooks.yaml +++ b/config/kubekey/crds/kubekey.kubesphere.io_playbooks.yaml @@ -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 diff --git a/docs/zh/004-task.md b/docs/zh/004-task.md index ae597f79..4af6c4d9 100644 --- a/docs/zh/004-task.md +++ b/docs/zh/004-task.md @@ -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) \ No newline at end of file diff --git a/docs/zh/modules/result.md b/docs/zh/modules/result.md new file mode 100644 index 00000000..90c4ecb2 --- /dev/null +++ b/docs/zh/modules/result.md @@ -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 +``` \ No newline at end of file diff --git a/pkg/executor/playbook_executor.go b/pkg/executor/playbook_executor.go index be1e3e0e..897325ab 100644 --- a/pkg/executor/playbook_executor.go +++ b/pkg/executor/playbook_executor.go @@ -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 { diff --git a/pkg/executor/task_executor.go b/pkg/executor/task_executor.go index ec1138ca..606a0be7 100644 --- a/pkg/executor/task_executor.go +++ b/pkg/executor/task_executor.go @@ -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) } diff --git a/pkg/modules/result.go b/pkg/modules/result.go new file mode 100644 index 00000000..4ce8a48c --- /dev/null +++ b/pkg/modules/result.go @@ -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)) +} diff --git a/pkg/modules/result_test.go b/pkg/modules/result_test.go new file mode 100644 index 00000000..cd70bd51 --- /dev/null +++ b/pkg/modules/result_test.go @@ -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) + }) + } +} diff --git a/pkg/variable/variable.go b/pkg/variable/variable.go index dbaaeb81..bfb5197e 100644 --- a/pkg/variable/variable.go +++ b/pkg/variable/variable.go @@ -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. diff --git a/pkg/variable/variable_get.go b/pkg/variable/variable_get.go index 4a906cab..35ca8cfe 100644 --- a/pkg/variable/variable_get.go +++ b/pkg/variable/variable_get.go @@ -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 + } +} diff --git a/pkg/variable/variable_merge.go b/pkg/variable/variable_merge.go index 2a87ffc7..09888f18 100644 --- a/pkg/variable/variable_merge.go +++ b/pkg/variable/variable_merge.go @@ -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 + } +} diff --git a/pkg/variable/variable_merge_test.go b/pkg/variable/variable_merge_test.go index 5276f038..11ad62f8 100644 --- a/pkg/variable/variable_merge_test.go +++ b/pkg/variable/variable_merge_test.go @@ -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) + }) + } +}