fix: add index to groups (#2649)

Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
liujian 2025-07-07 11:22:20 +08:00 committed by GitHub
parent e3dec872f6
commit c71814aa09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 141 additions and 114 deletions

View File

@ -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

View File

@ -1,8 +1,22 @@
- name: Check Connect
hosts:
- all
gather_facts: true
tasks:
- name: get_arch
debug:
msg: "{{ .os.architecture }}"
- 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 }}

View File

@ -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,

View File

@ -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 + "-",

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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 {