mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-25 17:12:50 +00:00
add result module (#2646)
Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
parent
794d28c706
commit
bbb8a4a031
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue