package roles import ( "fmt" "strings" "xcecli/common/output" "xcecli/common/request" "xcecli/config" logger "xcecli/config" role_filter "xcecli/filters" "xcecli/models" "github.com/spf13/cobra" "go.uber.org/zap" ) type RolesQuerier struct { configManager *config.ConfigManager filter *role_filter.RolesFilter fields string jsonOutput bool noHeader bool verbose bool wideFormat bool // wide format } func NewRolesQuerier(configManager *config.ConfigManager) *RolesQuerier { return &RolesQuerier{ configManager: configManager, filter: &role_filter.RolesFilter{}, } } func NewListRolesCmd(configManager *config.ConfigManager) *cobra.Command { rolesQuerier := NewRolesQuerier(configManager) cmd := &cobra.Command{ Use: "list", Short: "Query role info", Long: `Query role info, support filter by role. example: xce roles list # query all roles xce roles list -l # query all roles with detail xce roles list -w # list roles with wide format`, RunE: func(cmd *cobra.Command, args []string) error { if err := rolesQuerier.ParseCmdArgs(cmd, args); err != nil { fmt.Println(strings.ToLower(err.Error())) return nil } if err := rolesQuerier.RunRolesInfo("list"); err != nil { fmt.Println(strings.ToLower(err.Error())) return nil } return nil }, } // Add command line parameters flags := cmd.Flags() flags.StringVarP(&rolesQuerier.filter.RoleName, "role", "r", "", "filter by role name") flags.StringVarP(&rolesQuerier.filter.Type, "type", "t", "", "output for all or builtin roles") //flags.StringVarP(&rolesQuerier.fields, "output", "o", "", "output fields for roles") flags.BoolVarP(&rolesQuerier.wideFormat, "wided", "w", false, "output in wide format") flags.BoolVarP(&rolesQuerier.jsonOutput, "json", "", false, "output in JSON format") flags.BoolVarP(&rolesQuerier.noHeader, "noheader", "n", false, "output no header") return cmd } func (rq *RolesQuerier) ParseCmdArgs(cmd *cobra.Command, args []string) error { // No additional processing needed, all parameters are bound in flags if rq.filter.Type != "" && rq.filter.Type != "all" && rq.filter.Type != "builtin" { return fmt.Errorf("type must be either 'all' or 'builtin'") } if len(args) > 0 { rq.filter.RoleName = strings.Join(args, ",") } return nil } func (rq *RolesQuerier) RunRolesInfo(commandType string) error { // Get server information serverInfo, err := rq.configManager.GetServerInfo() if err != nil { return fmt.Errorf("failed to get server info: %w", err) } // create API client apiClient := request.NewAPIClient(serverInfo.URL) apiClient.SetRootCAs(serverInfo.CACert) // Create query parameters params := make(map[string]string) // Get filter string filterStr, err := rq.filter.HandleRoleFilter() if err != nil { errMsg := request.ExtractErrorMessage(err) return fmt.Errorf("failed to get filter: [%s]", errMsg) } else if filterStr != "" { params["filter"] = filterStr } // Field mapping // Process field selection // for list, only display role_name column; deduplication is done client-side if commandType == "list" { rq.fields = "ROLE_NAME" params["fields"] = "role_name" } else { fieldsMap := models.RoleFieldsMap if rq.verbose && rq.fields == "" { for header := range fieldsMap { rq.fields += header + "," } rq.fields = strings.TrimSuffix(rq.fields, ",") } if rq.fields != "" { rq.fields = request.CleanFields(rq.fields) mappedFields, err := request.CheckAndMapFields(rq.fields, fieldsMap) if err != nil { return err } params["fields"] = strings.Join(mappedFields, ",") } } if rq.filter.Type != "" { params["type"] = rq.filter.Type } // create page printer pagePrinter := output.NewPagePrinter(output.DataTypeRoles, output.WithFields(rq.fields), output.WithFormatJson(rq.jsonOutput), output.WithNoHeader(rq.noHeader), output.WithVerbose(rq.verbose), output.WithWideFormat(rq.wideFormat)) // Record if the first page has been processed isFirstPage := true var ( resp models.RolesResponse loopCount int pageToken string seen map[string]struct{} // cross-page dedup state for list mode ) if commandType == "list" { seen = make(map[string]struct{}) } // Loop to get all pages of data for { if loopCount > 0 && pageToken == "" { break } resp = models.RolesResponse{} // reset to avoid stale rows from previous page err = apiClient.GetRequestFromAPIServer(serverInfo.Token, pageToken, serverInfo.CACert, "/xce/v1/roles", &resp, params) if err != nil { return err } if resp.Data.Count == 0 { if isFirstPage { fmt.Println("no role found") return nil } } // For list mode, deduplicate role names client-side before printing if commandType == "list" { var unique []models.RoleInfo for _, row := range resp.Data.Rows { if _, ok := seen[row.RoleName]; !ok { seen[row.RoleName] = struct{}{} unique = append(unique, models.RoleInfo{RoleName: row.RoleName}) } } resp.Data.Rows = unique resp.Data.Count = len(unique) } // Process the first page if isFirstPage { // Start printing process pagePrinter.StartPrinting() // Prepare for printing, calculate column width and table header, etc. pagePrinter.PrepareForPrinting(&resp) isFirstPage = false } // Print the current page pagePrinter.PrintPage(&resp) // Update page token and loop counter pageToken = resp.Data.PageToken loopCount++ } // Finish printing pagePrinter.Finish() logger.Debug("roles response completed", zap.Int("pages", loopCount)) return nil } // uniqueRoleNames deduplicates role names from a RolesResponse, preserving order. // The server returns one row per permission, so the same role_name appears multiple times. func uniqueRoleNames(resp *models.RolesResponse) []string { seen := make(map[string]struct{}) var result []string for _, row := range resp.Data.Rows { if _, ok := seen[row.RoleName]; !ok { seen[row.RoleName] = struct{}{} result = append(result, row.RoleName) } } return result }