Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 172 additions & 15 deletions cmd/list/aliases.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package list

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
Expand All @@ -11,12 +14,24 @@ import (

const aliasesPageSize = 1000

type aliasesFetchOptions struct {
includeLabels bool
labelFilter []string
}

func aliasesCmd() *cobra.Command {
return &cobra.Command{
var withLabels bool
var labelFilter []string

cmd := &cobra.Command{
Use: "aliases",
Short: "Print every private host alias you can reach in the current organization",
Long: `Lists all private site aliases you have access to in your selected organization—one name per line.`,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("labels") {
withLabels = true
}

apiClient := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())

Expand All @@ -25,22 +40,164 @@ func aliasesCmd() *cobra.Command {
return err
}

for page := 1; ; page++ {
data, err := apiClient.ListUserResourceAliases(orgID, page, aliasesPageSize)
if err != nil {
return err
}
for _, a := range data.Aliases {
fmt.Println(a)
}
if len(data.Aliases) == 0 {
break
}
if len(data.Aliases) < aliasesPageSize {
break
}
requested := aliasesFetchOptions{
includeLabels: withLabels,
labelFilter: labelFilter,
}

data, err := fetchAllAliases(apiClient, orgID, requested)
if err != nil {
return err
}

printAliases(data, withLabels)
return nil
},
}

cmd.Flags().StringSliceVarP(&labelFilter, "label", "l", nil, "Only list aliases for resources with this label (repeatable, OR)")
cmd.Flags().BoolVarP(&withLabels, "with-labels", "L", false, "Include label names in output (alias and comma-separated labels, tab-separated)")
_ = cmd.Flags().Bool("labels", false, "Deprecated: use --with-labels")
_ = cmd.Flags().MarkDeprecated("labels", "use --with-labels instead")

return cmd
}

func fetchAllAliases(apiClient *api.Client, orgID string, requested aliasesFetchOptions) (*api.ListUserResourceAliasesData, error) {
effective, clientSideFilter, err := resolveAliasesFetchOptions(apiClient, orgID, requested)
if err != nil {
return nil, err
}

var combined api.ListUserResourceAliasesData
for page := 1; ; page++ {
pageData, err := apiClient.ListUserResourceAliases(orgID, page, aliasesPageSize, api.ListUserResourceAliasesOptions{
IncludeLabels: effective.includeLabels,
LabelFilter: effective.labelFilter,
})
if err != nil {
return nil, err
}

combined.Aliases = append(combined.Aliases, pageData.Aliases...)
combined.Items = append(combined.Items, pageData.Items...)
combined.Pagination = pageData.Pagination

if len(pageData.Aliases) == 0 {
break
}
if len(pageData.Aliases) < aliasesPageSize {
break
}
}

if clientSideFilter {
combined = *filterAliasesByLabels(&combined, requested.labelFilter)
}

return &combined, nil
}

func resolveAliasesFetchOptions(apiClient *api.Client, orgID string, requested aliasesFetchOptions) (aliasesFetchOptions, bool, error) {
effective := requested

if err := probeAliasesPage(apiClient, orgID, effective); err == nil {
return effective, false, nil
} else if !isBadRequest(err) {
return effective, false, err
}

if len(requested.labelFilter) > 0 {
effective = aliasesFetchOptions{
includeLabels: true,
labelFilter: nil,
}
if err := probeAliasesPage(apiClient, orgID, effective); err == nil {
return effective, true, nil
} else if !isBadRequest(err) {
return effective, false, err
}
}

effective = aliasesFetchOptions{}
if err := probeAliasesPage(apiClient, orgID, effective); err != nil {
return effective, false, err
}

return effective, false, nil
}

func probeAliasesPage(apiClient *api.Client, orgID string, opts aliasesFetchOptions) error {
_, err := apiClient.ListUserResourceAliases(orgID, 1, 1, api.ListUserResourceAliasesOptions{
IncludeLabels: opts.includeLabels,
LabelFilter: opts.labelFilter,
})
return err
}

func isBadRequest(err error) bool {
var apiErr *api.ErrorResponse
return errors.As(err, &apiErr) && apiErr.Status == http.StatusBadRequest
}

func filterAliasesByLabels(data *api.ListUserResourceAliasesData, labelFilter []string) *api.ListUserResourceAliasesData {
if len(labelFilter) == 0 {
return data
}

filterSet := make(map[string]struct{}, len(labelFilter))
for _, label := range labelFilter {
filterSet[label] = struct{}{}
}

if len(data.Items) == 0 {
return data
}

filteredItems := make([]api.UserResourceAliasItem, 0, len(data.Items))
for _, item := range data.Items {
if aliasMatchesLabelFilter(item.Labels, filterSet) {
filteredItems = append(filteredItems, item)
}
}

aliases := make([]string, len(filteredItems))
for i, item := range filteredItems {
aliases[i] = item.Alias
}

return &api.ListUserResourceAliasesData{
Aliases: aliases,
Items: filteredItems,
Pagination: data.Pagination,
}
}

func aliasMatchesLabelFilter(labels []string, filterSet map[string]struct{}) bool {
for _, label := range labels {
if _, ok := filterSet[label]; ok {
return true
}
}
return false
}

func printAliases(data *api.ListUserResourceAliasesData, withLabels bool) {
if !withLabels {
for _, alias := range data.Aliases {
fmt.Println(alias)
}
return
}

if len(data.Items) > 0 {
for _, item := range data.Items {
fmt.Printf("%s\t%s\n", item.Alias, strings.Join(item.Labels, ","))
}
return
}

for _, alias := range data.Aliases {
fmt.Printf("%s\t\n", alias)
}
}
25 changes: 18 additions & 7 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,17 +352,28 @@ func (c *Client) CheckOrgUserAccess(orgID, userID string) (*CheckOrgUserAccessRe
return &response, nil
}

// ListUserResourceAliasesOptions configures GET /org/:orgId/user-resource-aliases.
type ListUserResourceAliasesOptions struct {
IncludeLabels bool
LabelFilter []string
}

// ListUserResourceAliases returns one page of host-mode private site resource aliases for the user in the org.
func (c *Client) ListUserResourceAliases(orgID string, page, pageSize int) (*ListUserResourceAliasesData, error) {
func (c *Client) ListUserResourceAliases(orgID string, page, pageSize int, opts ListUserResourceAliasesOptions) (*ListUserResourceAliasesData, error) {
path := fmt.Sprintf("/org/%s/user-resource-aliases", url.PathEscape(orgID))
var data ListUserResourceAliasesData
opts := RequestOptions{
Query: map[string]string{
"page": strconv.Itoa(page),
"pageSize": strconv.Itoa(pageSize),
},
query := map[string]string{
"page": strconv.Itoa(page),
"pageSize": strconv.Itoa(pageSize),
}
if opts.IncludeLabels {
query["includeLabels"] = "true"
}
if len(opts.LabelFilter) > 0 {
query["labels"] = strings.Join(opts.LabelFilter, ",")
}
if err := c.Get(path, &data, opts); err != nil {
reqOpts := RequestOptions{Query: query}
if err := c.Get(path, &data, reqOpts); err != nil {
return nil, err
}
return &data, nil
Expand Down
11 changes: 9 additions & 2 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,17 @@ type SignSSHKeyResponse struct {
Data SignSSHKeyData `json:"data"`
}

// UserResourceAliasItem is one alias with optional label names from includeLabels=true.
type UserResourceAliasItem struct {
Alias string `json:"alias"`
Labels []string `json:"labels"`
}

// ListUserResourceAliasesData is the inner `data` of GET /org/:orgId/user-resource-aliases.
type ListUserResourceAliasesData struct {
Aliases []string `json:"aliases"`
Pagination AliasesPagination `json:"pagination"`
Aliases []string `json:"aliases"`
Items []UserResourceAliasItem `json:"items,omitempty"`
Pagination AliasesPagination `json:"pagination"`
}

// AliasesPagination matches the paginated API envelope for user-resource-aliases.
Expand Down
Loading