diff --git a/.golangci.yaml b/.golangci.yaml index 05c62428..aed01c94 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -113,7 +113,7 @@ linters-settings: cyclop: # The maximal code complexity to report. # Default: 10 - max-complexity: 20 + max-complexity: 25 # Should ignore tests. # Default: false skip-tests: true diff --git a/builtin/core/playbooks/host_check.yaml b/builtin/core/playbooks/host_check.yaml index 3c23dea2..be1eb409 100644 --- a/builtin/core/playbooks/host_check.yaml +++ b/builtin/core/playbooks/host_check.yaml @@ -1,8 +1,22 @@ - name: Check Connect hosts: - all - gather_facts: true tasks: - - name: get_arch - debug: - msg: "{{ .os.architecture }}" \ No newline at end of file + - name: get host info + ignore_errors: true + setup: {} + - name: set result + run_once: true + vars: + architectures: + amd64: + - amd64 + - x86_64 + arm64: + - arm64 + - aarch64 + result: | + {{- range $k,$v := .hostvars }} + {{ $k }}: {{ if $.architectures.amd64 | has ($v.os.architecture | default "") }}amd64{{ else if $.architectures.arm64 | has .os.architecture }}arm64{{ else }}{{ $v.os.architecture | default "" }}{{ end }} + {{- end }} + \ No newline at end of file diff --git a/pkg/const/env.go b/pkg/const/env.go index bf369ae8..ed829ae6 100644 --- a/pkg/const/env.go +++ b/pkg/const/env.go @@ -31,11 +31,6 @@ var ( CapkkVolumeProject = Environment{env: "CAPKK_VOLUME_PROJECT"} // CapkkVolumeWorkdir specifies the working directory for capkk playbook CapkkVolumeWorkdir = Environment{env: "CAPKK_VOLUME_WORKDIR"} - - // TaskNameGatherFacts the task name for gather_facts in playbook - TaskNameGatherFacts = Environment{env: "TASK_GATHER_FACTS", def: "gather_facts"} - // TaskNameGetArch the task name for get_arch in playbook, used to get host architecture - TaskNameGetArch = Environment{env: "", def: "get_arch"} ) // Getenv retrieves the value of the environment variable. If the environment variable is not set, diff --git a/pkg/executor/playbook_executor.go b/pkg/executor/playbook_executor.go index 897325ab..9833a433 100644 --- a/pkg/executor/playbook_executor.go +++ b/pkg/executor/playbook_executor.go @@ -246,8 +246,7 @@ func (e playbookExecutor) dealGatherFacts(ctx context.Context, gatherFacts bool, // skip return nil } - // run as option - + // run setup task return (&taskExecutor{option: e.option, task: &kkcorev1alpha1.Task{ ObjectMeta: metav1.ObjectMeta{ GenerateName: e.playbook.Name + "-", diff --git a/pkg/modules/command.go b/pkg/modules/command.go index 26f66301..6f516652 100644 --- a/pkg/modules/command.go +++ b/pkg/modules/command.go @@ -82,7 +82,7 @@ func ModuleCommand(ctx context.Context, options ExecOptions) (string, string) { } // execute command var stdout, stderr string - data, err := conn.ExecuteCommand(ctx, command) + data, err := conn.ExecuteCommand(ctx, string(command)) if err != nil { stderr = err.Error() } diff --git a/pkg/modules/result.go b/pkg/modules/result.go index 4ce8a48c..1dd0476a 100644 --- a/pkg/modules/result.go +++ b/pkg/modules/result.go @@ -47,13 +47,19 @@ Return Values: // ModuleResult handles the "result" module, setting result variables during playbook execution func ModuleResult(ctx context.Context, options ExecOptions) (string, string) { - var node yaml.Node + // get host variable + ha, err := options.getAllVariables() + if err != nil { + return "", err.Error() + } + arg, _ := variable.Extension2String(ha, options.Args) + var result any // Unmarshal the YAML document into a root node. - if err := yaml.Unmarshal(options.Args.Raw, &node); err != nil { + if err := yaml.Unmarshal(arg, &result); err != nil { return "", fmt.Sprintf("failed to unmarshal YAML error: %v", err) } - if err := options.Variable.Merge(variable.MergeResultVariable(node, options.Host)); err != nil { + if err := options.Variable.Merge(variable.MergeResultVariable(result)); err != nil { return "", fmt.Sprintf("result error: %v", err) } diff --git a/pkg/variable/helper.go b/pkg/variable/helper.go index c5c33018..e8e8a31e 100644 --- a/pkg/variable/helper.go +++ b/pkg/variable/helper.go @@ -399,30 +399,26 @@ func Extension2Slice(ctx map[string]any, ext runtime.RawExtension) []any { klog.ErrorS(err, "extension2string error", "input", string(ext.Raw)) } - if err := json.Unmarshal([]byte(val), &data); err == nil { + if err := json.Unmarshal(val, &data); err == nil { return data } return []any{val} } -// Extension2String convert runtime.RawExtension to string. -// if runtime.RawExtension contains tmpl syntax, parse it. -func Extension2String(ctx map[string]any, ext runtime.RawExtension) (string, error) { +// Extension2String converts a runtime.RawExtension to a string, optionally parsing it as a template. +// If the extension is empty, it returns nil. If the string is quoted, it unquotes it first. +// Finally, it parses the string as a template using the provided context. +func Extension2String(ctx map[string]any, ext runtime.RawExtension) ([]byte, error) { if len(ext.Raw) == 0 { - return "", nil + return nil, nil } var input = string(ext.Raw) - // try to escape string - if ns, err := strconv.Unquote(string(ext.Raw)); err == nil { + // try to escape string if it's quoted + if ns, err := strconv.Unquote(input); err == nil { input = ns } - result, err := tmpl.Parse(ctx, input) - if err != nil { - return "", err - } - - return string(result), nil + return tmpl.Parse(ctx, input) } diff --git a/pkg/variable/variable.go b/pkg/variable/variable.go index bfb5197e..dc73b76e 100644 --- a/pkg/variable/variable.go +++ b/pkg/variable/variable.go @@ -139,13 +139,19 @@ type variable struct { sync.RWMutex } -// value is the specific data contained in the variable +// resultKey is the key used to store global result variables in the value struct. +// It is used as the only key in the Result map to hold result data set by result tasks. +const resultKey = "result" + +// value holds all variable data for a playbook execution. +// It contains configuration, inventory, per-host variables, and global result variables. type value struct { - Config kkcorev1.Config - Inventory kkcorev1.Inventory - // Hosts store the variable for running tasks on specific hosts + Config kkcorev1.Config // Playbook configuration + Inventory kkcorev1.Inventory // Playbook inventory + // Hosts stores variables for each host, including remote and runtime variables. Hosts map[string]host - // result store the variable which set by result task. + // Result stores global result variables set by result tasks. + // The map always contains a single key (resultKey) for easy cloning and merging. Result map[string]any } diff --git a/pkg/variable/variable_get.go b/pkg/variable/variable_get.go index 35ca8cfe..edc85cae 100644 --- a/pkg/variable/variable_get.go +++ b/pkg/variable/variable_get.go @@ -223,6 +223,6 @@ var GetResultVariable = func() GetFunc { return nil, errors.New("variable type error") } - return vv.value.Result, nil + return vv.value.Result[resultKey], nil } } diff --git a/pkg/variable/variable_merge.go b/pkg/variable/variable_merge.go index 09888f18..85c486d0 100644 --- a/pkg/variable/variable_merge.go +++ b/pkg/variable/variable_merge.go @@ -118,32 +118,14 @@ var MergeHostsRuntimeVariable = func(node yaml.Node, hostname string, hosts ...s // 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 - } - +var MergeResultVariable = func(result any) MergeFunc { 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) + vv.value.Result = CombineVariables(vv.value.Result, map[string]any{resultKey: result}) return nil } diff --git a/pkg/variable/variable_merge_test.go b/pkg/variable/variable_merge_test.go index 11ad62f8..004ce324 100644 --- a/pkg/variable/variable_merge_test.go +++ b/pkg/variable/variable_merge_test.go @@ -137,7 +137,7 @@ func TestMergeResultVariable(t *testing.T) { }, }, data: map[string]any{ - "v1": "{{ .k1 }}", + "v1": "v1", "v2": "vv", }, except: value{ @@ -154,8 +154,10 @@ func TestMergeResultVariable(t *testing.T) { }, }, Result: map[string]any{ - "v1": "v1", - "v2": "vv", + resultKey: map[string]any{ + "v1": "v1", + "v2": "vv", + }, }, }, }, @@ -163,12 +165,7 @@ func TestMergeResultVariable(t *testing.T) { 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 { + if err := tc.variable.Merge(MergeResultVariable(tc.data)); err != nil { t.Fatal(err) } diff --git a/pkg/web/api/result.go b/pkg/web/api/result.go index c2ea108c..17c44d6f 100644 --- a/pkg/web/api/result.go +++ b/pkg/web/api/result.go @@ -16,9 +16,16 @@ limitations under the License. package api +const ( + // ResultSucceed indicates a successful operation result. + ResultSucceed = "success" + // ResultFailed indicates a failed operation result. + ResultFailed = "failed" +) + // SUCCESS is a global variable representing a successful operation result with a default success message. // It can be used as a standard response for successful API calls. -var SUCCESS = Result{Message: "success"} +var SUCCESS = Result{Message: ResultSucceed} // Result represents a basic API response structure containing a message field. // The Message field is typically used to convey error or success information. @@ -37,18 +44,25 @@ type ListResult[T any] struct { // InventoryHostTable represents a host entry in an inventory with its configuration details. // It includes network information, SSH credentials, group membership, and architecture. type InventoryHostTable struct { - Name string `json:"name"` // Hostname of the inventory host - Status string `json:"status"` // Current status of the host - InternalIPV4 string `json:"internalIPV4"` // IPv4 address of the host - InternalIPV6 string `json:"internalIPV6"` // IPv6 address of the host - SSHHost string `json:"sshHost"` // SSH hostname for connection - SSHPort string `json:"sshPort"` // SSH port for connection - SSHUser string `json:"sshUser"` // SSH username for authentication - SSHPassword string `json:"sshPassword"` // SSH password for authentication - SSHPrivateKey string `json:"sshPrivateKey"` // SSH private key for authentication - Vars map[string]any `json:"vars"` // Additional host variables - Groups []string `json:"groups"` // Groups the host belongs to - Arch string `json:"arch"` // Architecture of the host + Name string `json:"name"` // Hostname of the inventory host + Status string `json:"status"` // Current status of the host + InternalIPV4 string `json:"internalIPV4"` // IPv4 address of the host + InternalIPV6 string `json:"internalIPV6"` // IPv6 address of the host + SSHHost string `json:"sshHost"` // SSH hostname for connection + SSHPort string `json:"sshPort"` // SSH port for connection + SSHUser string `json:"sshUser"` // SSH username for authentication + SSHPassword string `json:"sshPassword"` // SSH password for authentication + SSHPrivateKey string `json:"sshPrivateKey"` // SSH private key for authentication + Vars map[string]any `json:"vars"` // Additional host variables + Groups []InventoryHostGroups `json:"groups"` // Groups the host belongs to + Arch string `json:"arch"` // Architecture of the host +} + +// InventoryHostGroups represents the group information for a host in the inventory. +// Role is the name of the group, and Index is the index of the group to which the host belongs. +type InventoryHostGroups struct { + Role string `json:"role"` // the groups name + Index int `json:"index"` // the index of groups which hosts belong to } // SchemaTable represents schema metadata for a resource. diff --git a/pkg/web/corev1.go b/pkg/web/corev1.go index efb47629..e997e93a 100644 --- a/pkg/web/corev1.go +++ b/pkg/web/corev1.go @@ -318,10 +318,12 @@ func (h *coreHandler) inventoryInfo(request *restful.Request, response *restful. // listInventoryHosts lists all hosts in an inventory with their details // It includes information about SSH configuration, IP addresses, and group membership func (h *coreHandler) listInventoryHosts(request *restful.Request, response *restful.Response) { + // Parse query parameters from the request queryParam := query.ParseQueryParameter(request) namespace := request.PathParameter("namespace") name := request.PathParameter("inventory") + // Retrieve the inventory object from the cluster inventory := &kkcorev1.Inventory{} err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Namespace: namespace, Name: name}, inventory) if err != nil { @@ -329,15 +331,19 @@ func (h *coreHandler) listInventoryHosts(request *restful.Request, response *res return } - // Retrieve the list of tasks associated with the inventory - taskList := &kkcorev1alpha1.TaskList{} - _ = h.client.List(request.Request.Context(), taskList, ctrlclient.InNamespace(namespace), ctrlclient.MatchingFields{ - "playbook.name": inventory.Annotations[kkcorev1.HostCheckPlaybookAnnotation], - }) + // get host-check playbook if annotation exists + playbook := &kkcorev1.Playbook{} + if playbookName, ok := inventory.Annotations[kkcorev1.HostCheckPlaybookAnnotation]; ok { + if err := h.client.Get(request.Request.Context(), ctrlclient.ObjectKey{Name: playbookName, Namespace: inventory.Namespace}, playbook); err != nil { + klog.Warningf("cannot found host-check playbook for inventory %q", ctrlclient.ObjectKeyFromObject(inventory)) + } + } // buildHostItem constructs an InventoryHostTable from the hostname and raw extension buildHostItem := func(hostname string, raw runtime.RawExtension) api.InventoryHostTable { + // Convert the raw extension to a map of variables vars := variable.Extension2Variables(raw) + // Extract relevant fields from the variables internalIPV4, _ := variable.StringVar(nil, vars, _const.VariableIPv4) internalIPV6, _ := variable.StringVar(nil, vars, _const.VariableIPv6) sshHost, _ := variable.StringVar(nil, vars, _const.VariableConnector, _const.VariableConnectorHost) @@ -351,6 +357,7 @@ func (h *coreHandler) listInventoryHosts(request *restful.Request, response *res delete(vars, _const.VariableIPv6) delete(vars, _const.VariableConnector) + // Return the constructed InventoryHostTable return api.InventoryHostTable{ Name: hostname, InternalIPV4: internalIPV4, @@ -361,49 +368,61 @@ func (h *coreHandler) listInventoryHosts(request *restful.Request, response *res SSHPassword: sshPassword, SSHPrivateKey: sshPrivateKey, Vars: vars, - Groups: []string{}, + Groups: []api.InventoryHostGroups{}, } } // Convert inventory groups for host membership lookup groups := variable.ConvertGroup(*inventory) + // Helper to check if a host is in a group and get its index + getGroupIndex := func(groupName, hostName string) int { + for i, h := range inventory.Spec.Groups[groupName].Hosts { + if h == hostName { + return i + } + } + return -1 + } + // fillGroups adds group names to the InventoryHostTable item if the host is a member fillGroups := func(item *api.InventoryHostTable) { for groupName, hosts := range groups { - if groupName == _const.VariableGroupsAll || groupName == _const.VariableUnGrouped { + // Skip special groups + if groupName == _const.VariableGroupsAll || groupName == _const.VariableUnGrouped || groupName == "k8s_cluster" { continue } + // If the host is in the group, add the group info to the item if slices.Contains(hosts, item.Name) { - item.Groups = append(item.Groups, groupName) + g := api.InventoryHostGroups{ + Role: groupName, + Index: getGroupIndex(groupName, item.Name), + } + item.Groups = append(item.Groups, g) } } } - // fillTaskInfo populates status and architecture info for the host from task results - fillTaskInfo := func(item *api.InventoryHostTable) { - for _, task := range taskList.Items { - switch task.Name { - case _const.Getenv(_const.TaskNameGatherFacts): - for _, result := range task.Status.HostResults { - if result.Host == item.Name { - item.Status = result.Stdout - break - } - } - case _const.Getenv(_const.TaskNameGetArch): - for _, result := range task.Status.HostResults { - if result.Host == item.Name { - item.Arch = result.Stdout - break - } - } + // fillByPlaybook populates status and architecture info for the host from task results + fillByPlaybook := func(playbook kkcorev1.Playbook, item *api.InventoryHostTable) { + // Set status and architecture based on playbook phase and result + switch playbook.Status.Phase { + case kkcorev1.PlaybookPhaseFailed: + item.Status = api.ResultFailed + case kkcorev1.PlaybookPhaseSucceeded: + item.Status = api.ResultFailed + // Extract architecture info from playbook result + results := variable.Extension2Variables(playbook.Status.Result) + if arch, ok := results[item.Name].(string); ok && arch != "" { + item.Arch = arch + item.Status = api.ResultSucceed } } } // less is a comparison function for sorting InventoryHostTable items by a given field less := func(left, right api.InventoryHostTable, sortBy query.Field) bool { + // Compare fields for sorting leftVal := query.GetFieldByJSONTag(reflect.ValueOf(left), sortBy) rightVal := query.GetFieldByJSONTag(reflect.ValueOf(right), sortBy) switch leftVal.Kind() { @@ -418,21 +437,26 @@ func (h *coreHandler) listInventoryHosts(request *restful.Request, response *res // filter is a function to filter InventoryHostTable items based on query filters filter := func(o api.InventoryHostTable, f query.Filter) bool { + // Filter by string fields, otherwise always true val := query.GetFieldByJSONTag(reflect.ValueOf(o), f.Field) switch val.Kind() { case reflect.String: - return val.String() == string(f.Value) + return strings.Contains(val.String(), string(f.Value)) default: return true } } // Build the host table for the inventory - hostTable := make([]api.InventoryHostTable, 0) + hostTable := make([]api.InventoryHostTable, 0, len(inventory.Spec.Hosts)) for hostname, raw := range inventory.Spec.Hosts { + // Build the host item from raw data item := buildHostItem(hostname, raw) + // Fill in group membership fillGroups(&item) - fillTaskInfo(&item) + // Fill in playbook status and architecture + fillByPlaybook(*playbook, &item) + // Add the item to the host table hostTable = append(hostTable, item) } diff --git a/pkg/web/resources.go b/pkg/web/resources.go index db2596c2..f5f07836 100644 --- a/pkg/web/resources.go +++ b/pkg/web/resources.go @@ -164,13 +164,7 @@ func (h schemaHandler) listIP(request *restful.Request, response *restful.Respon val := query.GetFieldByJSONTag(reflect.ValueOf(o), f.Field) switch val.Kind() { case reflect.String: - return val.String() == string(f.Value) - case reflect.Int: - v, err := strconv.Atoi(string(f.Value)) - if err != nil { - return false - } - return v == int(val.Int()) + return strings.Contains(val.String(), string(f.Value)) default: return true } @@ -255,7 +249,7 @@ func (h schemaHandler) allSchema(request *restful.Request, response *restful.Res val := query.GetFieldByJSONTag(reflect.ValueOf(o), f.Field) switch val.Kind() { case reflect.String: - return val.String() == string(f.Value) + return strings.Contains(val.String(), string(f.Value)) case reflect.Int: v, err := strconv.Atoi(string(f.Value)) if err != nil {