server/admin/namespace.go
Rama master · réplica de lectura del repositorio original.
package admin
import (
"errors"
"fmt"
"slices"
"strconv"
"strings"
"text/tabwriter"
"github.com/shellhub-io/shellhub/pkg/api/authorizer"
"github.com/shellhub-io/shellhub/pkg/models"
"github.com/shellhub-io/shellhub/pkg/uuid"
"github.com/shellhub-io/shellhub/server/admin/inputs"
"github.com/shellhub-io/shellhub/server/admin/services"
"github.com/spf13/cobra"
)
func namespaceCommands(service serviceFunc) *cobra.Command {
cmd := &cobra.Command{
Use: "namespace",
Short: "Manage namespaces",
Long: `Provides an interface for managing namespaces within the system, such as creating new namespaces or deleting existing ones.`,
}
cmd.AddCommand(
namespaceCreate(service),
namespaceDelete(service),
namespaceLimit(service),
namespaceList(service),
namespaceInspect(service),
memberCommands(service),
)
return cmd
}
func namespaceCreate(service serviceFunc) *cobra.Command {
cmdNamespace := &cobra.Command{
Use: "create <namespace> <owner> [tenant]",
Short: "Create a namespace",
Long: `Creates a new namespace in the system using the provided namespace name, associated owner's username, and an optional tenant ID and Type.
The owner must be a valid username within the system. If a tenant ID is provided, it should be in UUID format.`,
Example: `./bin/cli namespace create dev john_doe --type=team`,
Args: cobra.RangeArgs(2, 3),
RunE: func(cmd *cobra.Command, args []string) error {
namespaceType, err := cmd.Flags().GetString("type")
if err != nil {
return err
}
input := inputs.NamespaceCreate{
Namespace: strings.ToLower(args[0]),
Owner: strings.ToLower(args[1]),
TenantID: "",
Type: strings.ToLower(namespaceType),
}
if len(args) == 3 {
input.TenantID = args[2]
}
if err := validateInput(input); err != nil {
return err
}
namespace, err := service().NamespaceCreate(cmd.Context(), &input)
if err != nil {
return err
}
cmd.Println("Namespace created successfully")
cmd.Println("Namespace:", namespace.Name)
cmd.Println("Tenant:", namespace.TenantID)
cmd.Println("Owner:", namespace.Owner)
cmd.Println("Type:", namespace.Type)
return nil
},
}
cmdNamespace.PersistentFlags().String("type", "team", "type")
return cmdNamespace
}
func namespaceDelete(service serviceFunc) *cobra.Command {
return &cobra.Command{
Use: "delete <namespace>",
Short: "Delete a namespace",
Long: `Deletes a namespace and all associated data from the system based on the provided name.`,
Example: `./bin/cli namespace delete dev`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
input := inputs.NamespaceDelete{
Namespace: strings.ToLower(args[0]),
}
if err := validateInput(input); err != nil {
return err
}
if err := service().NamespaceDelete(cmd.Context(), &input); err != nil {
return err
}
cmd.Println("Namespace deleted successfully")
cmd.Println("Namespace:", input.Namespace)
return nil
},
}
}
func namespaceLimit(service serviceFunc) *cobra.Command {
cmd := &cobra.Command{
Use: "limit <namespace> <max-devices>",
Short: "Set a namespace's device limit",
Long: `Sets how many accepted devices a namespace may hold. Use -1 to remove the limit.
Devices already accepted stay accepted; the namespace refuses the next accept once it holds the limit.`,
Example: `./bin/cli namespace limit dev 10
./bin/cli namespace limit dev -1`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
maxDevices, err := strconv.Atoi(args[1])
if err != nil {
return ErrInvalidMaxDevices
}
input := inputs.NamespaceLimit{
Namespace: strings.ToLower(args[0]),
MaxDevices: maxDevices,
}
if err := validateInput(input); err != nil {
return err
}
namespace, err := service().NamespaceLimit(cmd.Context(), &input)
if err != nil {
return err
}
cmd.Println("Namespace limit set successfully")
cmd.Println("Namespace:", namespace.Name)
cmd.Println("Tenant:", namespace.TenantID)
if namespace.MaxDevices == services.MaxNumberDevicesUnlimited {
cmd.Println("Max Devices: unlimited")
} else {
cmd.Println("Max Devices:", namespace.MaxDevices)
}
return nil
},
}
cmd.Flags().SetInterspersed(false)
return cmd
}
func namespaceList(service serviceFunc) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List namespaces",
Long: "List all namespaces in the system",
Example: `./bin/cli namespace list
./bin/cli namespace ls
./bin/cli namespace ls -q
./bin/cli namespace ls -q tenant-id`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
out := cmd.OutOrStdout()
validFields := map[string]bool{
"name": true,
"tenant-id": true,
}
quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
return err
}
field := "name"
if len(args) == 1 {
field = args[0]
}
if !quiet && len(args) == 1 {
return errors.New("field argument requires -q")
}
if !validFields[field] {
return fmt.Errorf("invalid field: %s (allowed: name, tenant-id)", field)
}
namespaces, err := service().NamespaceList(cmd.Context())
if err != nil {
return err
}
if len(namespaces) == 0 {
if !quiet {
_, _ = fmt.Fprintln(out, "No namespaces to list")
}
return nil
}
if quiet {
for _, ns := range namespaces {
var v string
if field == "tenant-id" {
v = ns.TenantID
} else {
v = ns.Name
}
_, _ = fmt.Fprintln(out, v)
}
return nil
}
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "NAME\tTENANT ID\tTYPE")
for _, ns := range namespaces {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", ns.Name, ns.TenantID, ns.Type)
}
_ = w.Flush()
return nil
},
}
cmd.Flags().BoolP("quiet", "q", false,
"Output only a single field (default: name, options: name, tenant-id)")
return cmd
}
func namespaceInspect(service serviceFunc) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect <namespace>",
Short: "Inspect a namespace",
Long: "Inspect a namespace by either name or tenant-id",
Example: `./bin/cli namespace inspect dev
./bin/cli namespace inspect --tenant-id 8f3c2e1a...
./bin/cli namespace inspect $(./bin/cli namespace ls -q | sed -n '2p')
./bin/cli namespace inspect --tenant-id $(./bin/cli namespace ls -q tenant-id | sed -n '2p')`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
out := cmd.OutOrStdout()
tenantID, err := cmd.Flags().GetString("tenant-id")
if err != nil {
return err
}
if tenantID != "" {
if _, err := uuid.Parse(tenantID); err != nil {
return errors.New("invalid tenant ID: must be a valid UUID")
}
}
if tenantID != "" && len(args) > 0 {
return errors.New("cannot provide both a namespace name and --tenant-id")
}
if len(args) > 0 {
if _, err := uuid.Parse(args[0]); err == nil {
return errors.New("it looks like you provided a tenant ID; use the --tenant-id flag")
}
}
if tenantID == "" && len(args) == 0 {
return errors.New("please provide either a namespace name or --tenant-id")
}
resolver := services.NamespaceResolverName
var value string
if tenantID != "" {
resolver = services.NamespaceResolverTenantID
value = tenantID
} else {
value = strings.ToLower(args[0])
}
ns, err := service().NamespaceResolve(cmd.Context(), resolver, value)
if err != nil {
return err
}
users, err := service().UserList(cmd.Context())
if err != nil {
return err
}
userMap := make(map[string]models.User, len(users))
for _, u := range users {
userMap[u.ID] = u
}
owner, ok := userMap[ns.Owner]
if !ok {
return fmt.Errorf("owner %s not found", ns.Owner)
}
counts, err := service().NamespaceDeviceCounts(cmd.Context(), ns.TenantID)
if err != nil {
return err
}
_, _ = fmt.Fprintf(out, `Namespace:
Name %s
Type %s
Owner %s
Tenant ID %s
Created At %s
Devices:
Accepted %d
Online %d
Pending %d
Rejected %d
Members (%d):
`,
ns.Name,
ns.Type,
owner.Username,
ns.TenantID,
ns.CreatedAt.Format("2006-01-02"),
counts.RegisteredDevices,
counts.OnlineDevices,
counts.PendingDevices,
counts.RejectedDevices,
len(ns.Members),
)
slices.SortFunc(ns.Members, func(a, b models.Member) int {
return strings.Compare(userMap[a.ID].Email, userMap[b.ID].Email)
})
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
for _, m := range ns.Members {
user, ok := userMap[m.ID]
if !ok {
return fmt.Errorf("member %s not found", m.ID)
}
_, _ = fmt.Fprintf(w, " %s\t%s\n", user.Email, m.Role)
}
_ = w.Flush()
_, _ = fmt.Fprintln(out, "\nLimits:")
if ns.MaxDevices == -1 {
_, _ = fmt.Fprintln(out, " Max Devices unlimited")
} else {
_, _ = fmt.Fprintf(out, " Max Devices %d\n", ns.MaxDevices)
}
return nil
},
}
cmd.Flags().String("tenant-id", "", "Inspect namespace by tenant ID")
return cmd
}
func memberCommands(service serviceFunc) *cobra.Command {
cmd := &cobra.Command{
Use: "member",
Short: "Manage members",
Long: `Manage members`,
}
cmd.AddCommand(memberAdd(service))
cmd.AddCommand(memberRemove(service))
return cmd
}
func memberAdd(service serviceFunc) *cobra.Command {
return &cobra.Command{
Use: "add <username> <namespace> <role>",
Short: "Add a member",
Long: `Adds a new member to the specified namespace with the given role.
The username identifies the member to be added, the namespace specifies where the member should be added,
and the role indicates the permissions that the member will have within that namespace.`,
Example: `./bin/cli namespace member add myuser mynamespace observer`,
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
role := authorizer.RoleFromString(args[2])
if !role.Assignable() {
return fmt.Errorf("invalid role %q, valid roles are: owner, administrator, operator, observer", args[2])
}
input := inputs.MemberAdd{
Username: strings.ToLower(args[0]),
Namespace: strings.ToLower(args[1]),
Role: role,
}
if err := validateInput(input); err != nil {
return err
}
namespace, err := service().NamespaceAddMember(cmd.Context(), &input)
if err != nil {
return err
}
cmd.Println("Member added successfully")
cmd.Println("Namespace:", namespace.Name)
cmd.Println("Tenant:", namespace.TenantID)
cmd.Println("Member:", input.Username)
cmd.Println("Role:", input.Role)
return nil
},
}
}
func memberRemove(service serviceFunc) *cobra.Command {
return &cobra.Command{
Use: "remove <username> <namespace>",
Short: "Remove a member",
Long: `Removes an existing member from the specified namespace.
The username identifies the member to be removed, and the namespace specifies where the member is currently located.`,
Example: `./bin/cli namespace member remove john_doe dev`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
input := inputs.MemberRemove{
Username: strings.ToLower(args[0]),
Namespace: strings.ToLower(args[1]),
}
if err := validateInput(input); err != nil {
return err
}
namespace, err := service().NamespaceRemoveMember(cmd.Context(), &input)
if err != nil {
return err
}
cmd.Println("Member removed successfully")
cmd.Println("Namespace:", namespace.Name)
cmd.Println("Tenant:", namespace.TenantID)
cmd.Println("Member:", input.Username)
return nil
},
}
}