Skip to content

Commit 1790736

Browse files
committed
feat: enhance playbook execution and query handling
- Added a new optional query parameter `promise` to the playbook and inventory endpoints, allowing for asynchronous execution control. - Introduced a new result state `ResultPending` to indicate ongoing operations. - Refactored the executor function to handle the `promise` parameter, enabling conditional execution of playbooks. - Improved error handling and logging during playbook execution. Signed-off-by: joyceliu <joyceliu@yunify.com>
1 parent 1c8023a commit 1790736

7 files changed

Lines changed: 115 additions & 111 deletions

File tree

pkg/web/api/result.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const (
3131
ResultSucceed = "success"
3232
// ResultFailed indicates a failed operation result.
3333
ResultFailed = "failed"
34+
// ResultPending indicates a pending operation result.
35+
ResultPending = "pending"
3436
)
3537

3638
// SUCCESS is a global variable representing a successful operation result with a default success message.

pkg/web/handler/executor.go

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,46 +26,57 @@ type manager struct {
2626
manager map[string]context.CancelFunc // Map of playbook key to its cancel function
2727
}
2828

29-
func (m *manager) executor(playbook *kkcorev1.Playbook, client ctrlclient.Client) error {
30-
// Build the log file path for the playbook execution
31-
filename := filepath.Join(
32-
_const.GetWorkdirFromConfig(playbook.Spec.Config),
33-
_const.RuntimeDir,
34-
kkcorev1.SchemeGroupVersion.Group,
35-
kkcorev1.SchemeGroupVersion.Version,
36-
"playbooks",
37-
playbook.Namespace,
38-
playbook.Name,
39-
playbook.Name+".log",
40-
)
41-
// Ensure the directory for the log file exists
42-
if _, err := os.Stat(filepath.Dir(filename)); err != nil {
43-
if !os.IsNotExist(err) {
44-
return errors.Wrapf(err, "failed to stat playbook dir %q", filepath.Dir(filename))
29+
func (m *manager) executor(playbook *kkcorev1.Playbook, client ctrlclient.Client, promise string) error {
30+
f := func() error {
31+
// Build the log file path for the playbook execution
32+
filename := filepath.Join(
33+
_const.GetWorkdirFromConfig(playbook.Spec.Config),
34+
_const.RuntimeDir,
35+
kkcorev1.SchemeGroupVersion.Group,
36+
kkcorev1.SchemeGroupVersion.Version,
37+
"playbooks",
38+
playbook.Namespace,
39+
playbook.Name,
40+
playbook.Name+".log",
41+
)
42+
// Ensure the directory for the log file exists
43+
if _, err := os.Stat(filepath.Dir(filename)); err != nil {
44+
if !os.IsNotExist(err) {
45+
return errors.Wrapf(err, "failed to stat playbook dir %q", filepath.Dir(filename))
46+
}
47+
// If directory does not exist, create it
48+
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
49+
return errors.Wrapf(err, "failed to create playbook dir %q", filepath.Dir(filename))
50+
}
4551
}
46-
// If directory does not exist, create it
47-
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
48-
return errors.Wrapf(err, "failed to create playbook dir %q", filepath.Dir(filename))
52+
// Open the log file for writing
53+
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
54+
if err != nil {
55+
return errors.Wrapf(err, "failed to open log file", "file", filename)
4956
}
50-
}
51-
// Open the log file for writing
52-
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
53-
if err != nil {
54-
return errors.Wrapf(err, "failed to open log file", "file", filename)
55-
}
56-
defer file.Close()
57+
defer file.Close()
5758

58-
// Create a cancellable context for playbook execution
59-
ctx, cancel := context.WithCancel(context.Background())
60-
// Register the playbook and its cancel function in the playbookManager
61-
m.addPlaybook(playbook, cancel)
62-
// Execute the playbook and write output to the log file
63-
if err := executor.NewPlaybookExecutor(ctx, client, playbook, file).Exec(ctx); err != nil {
64-
klog.ErrorS(err, "failed to exec playbook", "playbook", playbook.Name)
59+
// Create a cancellable context for playbook execution
60+
ctx, cancel := context.WithCancel(context.Background())
61+
// Register the playbook and its cancel function in the playbookManager
62+
m.addPlaybook(playbook, cancel)
63+
// Execute the playbook and write output to the log file
64+
if err := executor.NewPlaybookExecutor(ctx, client, playbook, file).Exec(ctx); err != nil {
65+
klog.ErrorS(err, "failed to exec playbook", "playbook", playbook.Name)
66+
}
67+
// Remove the playbook from the playbookManager after execution
68+
m.deletePlaybook(playbook)
69+
return nil
70+
}
71+
if promise == "true" {
72+
go func() {
73+
if err := f(); err != nil {
74+
klog.ErrorS(err, "failed to execute playbook", "playbook", ctrlclient.ObjectKeyFromObject(playbook))
75+
}
76+
}()
77+
return nil
6578
}
66-
// Remove the playbook from the playbookManager after execution
67-
m.deletePlaybook(playbook)
68-
return nil
79+
return f()
6980
}
7081

7182
// addPlaybook adds a playbook and its cancel function to the manager map.

pkg/web/handler/inventory.go

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ func NewInventoryHandler(workdir string, restconfig *rest.Config, client ctrlcli
4343
func (h *InventoryHandler) Post(request *restful.Request, response *restful.Response) {
4444
inventory := &kkcorev1.Inventory{}
4545
if err := request.ReadEntity(inventory); err != nil {
46-
api.HandleBadRequest(response, request, err)
46+
api.HandleError(response, request, err)
4747
return
4848
}
4949
if err := h.client.Create(request.Request.Context(), inventory); err != nil {
50-
api.HandleBadRequest(response, request, err)
50+
api.HandleError(response, request, err)
5151
return
5252
}
5353

@@ -61,21 +61,21 @@ func (h *InventoryHandler) Patch(request *restful.Request, response *restful.Res
6161
name := request.PathParameter("inventory")
6262
data, err := io.ReadAll(request.Request.Body)
6363
if err != nil {
64-
api.HandleBadRequest(response, request, err)
64+
api.HandleError(response, request, err)
6565
return
6666
}
6767
patchType := request.HeaderParameter("Content-Type")
6868

6969
// Get the existing inventory object.
7070
inventory := &kkcorev1.Inventory{}
7171
if err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, inventory); err != nil {
72-
api.HandleBadRequest(response, request, err)
72+
api.HandleError(response, request, err)
7373
return
7474
}
7575

7676
// Apply the patch.
7777
if err := h.client.Patch(request.Request.Context(), inventory, ctrlclient.RawPatch(types.PatchType(patchType), data)); err != nil {
78-
api.HandleBadRequest(response, request, err)
78+
api.HandleError(response, request, err)
7979
return
8080
}
8181
// create host-check playbook
@@ -98,28 +98,27 @@ func (h *InventoryHandler) Patch(request *restful.Request, response *restful.Res
9898
}
9999
// Set the workdir in the playbook's spec config
100100
if err := unstructured.SetNestedField(playbook.Spec.Config.Value(), h.workdir, _const.Workdir); err != nil {
101-
api.HandleBadRequest(response, request, err)
101+
api.HandleError(response, request, err)
102102
return
103103
}
104104
if err := h.client.Create(request.Request.Context(), playbook); err != nil {
105-
api.HandleBadRequest(response, request, errors.Wrap(err, "failed to create hostcheck playbook"))
105+
api.HandleError(response, request, errors.Wrap(err, "failed to create hostcheck playbook"))
106106
return
107107
}
108108

109109
// Execute the playbook asynchronously.
110-
if err := playbookManager.executor(playbook, h.client); err != nil {
111-
api.HandleBadRequest(response, request, errors.Wrap(err, "failed to execute hostcheck playbook"))
110+
if err := playbookManager.executor(playbook, h.client, query.DefaultString(request.QueryParameter("promise"), "true")); err != nil {
111+
api.HandleError(response, request, errors.Wrap(err, "failed to execute hostcheck playbook"))
112112
return
113113
}
114-
115114
// update inventory annotation
116115
old := inventory.DeepCopy()
117116
if inventory.Annotations == nil {
118117
inventory.Annotations = make(map[string]string)
119118
}
120119
inventory.Annotations[kkcorev1.HostCheckPlaybookAnnotation] = playbook.Name
121120
if err := h.client.Patch(request.Request.Context(), inventory, ctrlclient.MergeFrom(old)); err != nil {
122-
api.HandleBadRequest(response, request, errors.Wrap(err, "failed to execute hostcheck playbook"))
121+
api.HandleError(response, request, errors.Wrap(err, "failed to execute hostcheck playbook"))
123122
return
124123
}
125124

@@ -133,18 +132,17 @@ func (h *InventoryHandler) List(request *restful.Request, response *restful.Resp
133132
var fieldselector fields.Selector
134133
// Parse field selector from query parameters if present.
135134
if v, ok := queryParam.Filters[query.ParameterFieldSelector]; ok {
136-
fs, err := fields.ParseSelector(string(v))
135+
fs, err := fields.ParseSelector(v)
137136
if err != nil {
138137
api.HandleError(response, request, err)
139138
return
140139
}
141140
fieldselector = fs
142141
}
143-
namespace := request.PathParameter("namespace")
144142

145143
inventoryList := &kkcorev1.InventoryList{}
146144
// List inventory resources from the Kubernetes API.
147-
err := h.client.List(request.Request.Context(), inventoryList, &ctrlclient.ListOptions{Namespace: namespace, LabelSelector: queryParam.Selector(), FieldSelector: fieldselector})
145+
err := h.client.List(request.Request.Context(), inventoryList, &ctrlclient.ListOptions{Namespace: request.PathParameter("namespace"), LabelSelector: queryParam.Selector(), FieldSelector: fieldselector})
148146
if err != nil {
149147
api.HandleError(response, request, err)
150148
return
@@ -202,7 +200,6 @@ func (h *InventoryHandler) ListHosts(request *restful.Request, response *restful
202200
queryParam := query.ParseQueryParameter(request)
203201
namespace := request.PathParameter("namespace")
204202
name := request.PathParameter("inventory")
205-
206203
// Retrieve the inventory object from the cluster.
207204
inventory := &kkcorev1.Inventory{}
208205
err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, inventory)
@@ -287,6 +284,8 @@ func (h *InventoryHandler) ListHosts(request *restful.Request, response *restful
287284
fillByPlaybook := func(playbook kkcorev1.Playbook, item *api.InventoryHostTable) {
288285
// Set status and architecture based on playbook phase and result.
289286
switch playbook.Status.Phase {
287+
case kkcorev1.PlaybookPhasePending, kkcorev1.PlaybookPhaseRunning:
288+
item.Status = api.ResultPending
290289
case kkcorev1.PlaybookPhaseFailed:
291290
item.Status = api.ResultFailed
292291
case kkcorev1.PlaybookPhaseSucceeded:
@@ -321,7 +320,7 @@ func (h *InventoryHandler) ListHosts(request *restful.Request, response *restful
321320
val := query.GetFieldByJSONTag(reflect.ValueOf(o), f.Field)
322321
switch val.Kind() {
323322
case reflect.String:
324-
return strings.Contains(val.String(), string(f.Value))
323+
return strings.Contains(val.String(), f.Value)
325324
default:
326325
return true
327326
}

pkg/web/handler/playbook.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bufio"
55
"context"
66
"encoding/json"
7-
"errors"
87
"fmt"
98
"io"
109
"net/http"
@@ -13,14 +12,15 @@ import (
1312
"strings"
1413
"time"
1514

15+
"github.com/cockroachdb/errors"
1616
"github.com/emicklei/go-restful/v3"
1717
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
1818
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
19+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1920
"k8s.io/apimachinery/pkg/api/meta"
2021
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2122
"k8s.io/apimachinery/pkg/fields"
2223
"k8s.io/client-go/rest"
23-
"k8s.io/klog/v2"
2424
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
2525

2626
_const "github.com/kubesphere/kubekey/v4/pkg/const"
@@ -46,7 +46,7 @@ func (h *PlaybookHandler) Post(request *restful.Request, response *restful.Respo
4646
playbook := &kkcorev1.Playbook{}
4747
// Read the playbook entity from the request body
4848
if err := request.ReadEntity(playbook); err != nil {
49-
api.HandleBadRequest(response, request, err)
49+
api.HandleError(response, request, err)
5050
return
5151
}
5252

@@ -73,7 +73,7 @@ func (h *PlaybookHandler) Post(request *restful.Request, response *restful.Respo
7373
if err := h.client.List(request.Request.Context(), playbookList, ctrlclient.MatchingLabels{
7474
labelKey: labelValue,
7575
}); err != nil {
76-
api.HandleBadRequest(response, request, err)
76+
api.HandleError(response, request, err)
7777
return
7878
}
7979
// If any playbook with the same schema label exists, this is a conflict
@@ -85,22 +85,20 @@ func (h *PlaybookHandler) Post(request *restful.Request, response *restful.Respo
8585

8686
// Set the workdir in the playbook's spec config
8787
if err := unstructured.SetNestedField(playbook.Spec.Config.Value(), h.workdir, _const.Workdir); err != nil {
88-
api.HandleBadRequest(response, request, err)
88+
api.HandleError(response, request, err)
8989
return
9090
}
9191
playbook.Status.Phase = kkcorev1.PlaybookPhasePending
9292
// Create the playbook resource in Kubernetes
9393
if err := h.client.Create(context.TODO(), playbook); err != nil {
94-
api.HandleBadRequest(response, request, err)
94+
api.HandleError(response, request, err)
9595
return
9696
}
9797
// Start playbook execution in a separate goroutine
98-
go func() {
99-
if err := playbookManager.executor(playbook, h.client); err != nil {
100-
klog.ErrorS(err, "failed to executor playbook", "playbook", ctrlclient.ObjectKeyFromObject(playbook))
101-
}
102-
}()
103-
98+
if err := playbookManager.executor(playbook, h.client, query.DefaultString(request.QueryParameter("promise"), "true")); err != nil {
99+
api.HandleError(response, request, errors.Wrap(err, "failed to execute playbook"))
100+
return
101+
}
104102
// For web UI: it does not run in Kubernetes, so execute playbook immediately.
105103
_ = response.WriteEntity(playbook)
106104
}
@@ -112,23 +110,20 @@ func (h *PlaybookHandler) List(request *restful.Request, response *restful.Respo
112110
var fieldselector fields.Selector
113111
// Parse field selector from query parameters if present.
114112
if v, ok := queryParam.Filters[query.ParameterFieldSelector]; ok {
115-
fs, err := fields.ParseSelector(string(v))
113+
fs, err := fields.ParseSelector(v)
116114
if err != nil {
117115
api.HandleError(response, request, err)
118116
return
119117
}
120118
fieldselector = fs
121119
}
122-
namespace := request.PathParameter("namespace")
123-
124120
playbookList := &kkcorev1.PlaybookList{}
125121
// List playbooks from the Kubernetes API with the specified options.
126-
err := h.client.List(request.Request.Context(), playbookList, &ctrlclient.ListOptions{Namespace: namespace, LabelSelector: queryParam.Selector(), FieldSelector: fieldselector})
122+
err := h.client.List(request.Request.Context(), playbookList, &ctrlclient.ListOptions{Namespace: request.PathParameter("namespace"), LabelSelector: queryParam.Selector(), FieldSelector: fieldselector})
127123
if err != nil {
128124
api.HandleError(response, request, err)
129125
return
130126
}
131-
132127
// Sort and filter the playbook list using DefaultList.
133128
results := query.DefaultList(playbookList.Items, queryParam, func(left, right kkcorev1.Playbook, sortBy string) bool {
134129
leftMeta, err := meta.Accessor(left)
@@ -267,7 +262,11 @@ func (h *PlaybookHandler) Delete(request *restful.Request, response *restful.Res
267262
// Retrieve the playbook resource to delete.
268263
err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, playbook)
269264
if err != nil {
270-
api.HandleError(response, request, err)
265+
if apierrors.IsNotFound(err) {
266+
response.WriteEntity(api.SUCCESS)
267+
} else {
268+
api.HandleError(response, request, err)
269+
}
271270
return
272271
}
273272
// Stop the playbook execution if it is running.

0 commit comments

Comments
 (0)