feat(cli): add commands to manage users (#1691)
This commit is contained in:
192
cli/cmd/ctl/user/create.go
Normal file
192
cli/cmd/ctl/user/create.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytetrade.io/web3os/app-service/api/sys.bytetrade.io/v1alpha1"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/beclab/Olares/cli/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
iamv1alpha2 "github.com/beclab/api/iam/v1alpha2"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type createUserOptions struct {
|
||||
name string
|
||||
displayName string
|
||||
domain string
|
||||
role string
|
||||
resourceLimit
|
||||
password string
|
||||
description string
|
||||
kubeConfig string
|
||||
}
|
||||
|
||||
func NewCmdCreateUser() *cobra.Command {
|
||||
o := &createUserOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "create {name}",
|
||||
Aliases: []string{"add", "new"},
|
||||
Short: "create a new user",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.name = args[0]
|
||||
if err := o.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *createUserOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&o.displayName, "display-name", "", "display name (optional)")
|
||||
cmd.Flags().StringVar(&o.domain, "domain", "", "domain (optional, defaults to the Olares system's domain)")
|
||||
cmd.Flags().StringVarP(&o.role, "role", "r", "normal", "owner role (optional, one of owner, admin, normal)")
|
||||
cmd.Flags().StringVarP(&o.memoryLimit, "memory-limit", "m", defaultMemoryLimit, "memory limit (optional)")
|
||||
cmd.Flags().StringVarP(&o.cpuLimit, "cpu-limit", "c", defaultCPULimit, "cpu limit (optional)")
|
||||
cmd.Flags().StringVarP(&o.password, "password", "p", "", "initial password (optional)")
|
||||
cmd.Flags().StringVar(&o.description, "description", "", "user description (optional)")
|
||||
cmd.Flags().StringVar(&o.kubeConfig, "kubeconfig", "", "path to kubeconfig file (optional)")
|
||||
}
|
||||
|
||||
func (o *createUserOptions) Validate() error {
|
||||
if o.name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
|
||||
if errs := validation.IsDNS1123Subdomain(o.name); len(errs) > 0 {
|
||||
return fmt.Errorf("invalid name: %s", strings.Join(errs, ","))
|
||||
}
|
||||
|
||||
if o.domain != "" {
|
||||
if errs := validation.IsDNS1123Subdomain(o.domain); len(errs) > 0 {
|
||||
return fmt.Errorf("invalid domain: %s", strings.Join(errs, ","))
|
||||
}
|
||||
if len(strings.Split(o.domain, ".")) < 2 {
|
||||
return errors.New("invalid domain: should be a domain with at least two segments separated by dots")
|
||||
}
|
||||
for _, label := range strings.Split(o.domain, ".") {
|
||||
if errs := validation.IsDNS1123Label(label); len(errs) > 0 {
|
||||
return fmt.Errorf("invalid domain: %s", strings.Join(errs, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if o.role != "" {
|
||||
if o.role != roleOwner && o.role != roleAdmin && o.role != roleNormal {
|
||||
return fmt.Errorf("invalid role: should be one of owner, admin, or normal")
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateResourceLimit(o.resourceLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *createUserOptions) Run() error {
|
||||
ctx := context.Background()
|
||||
userClient, err := newUserClientFromKubeConfig(o.kubeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.memoryLimit == "" {
|
||||
o.memoryLimit = defaultMemoryLimit
|
||||
}
|
||||
|
||||
if o.cpuLimit == "" {
|
||||
o.cpuLimit = defaultCPULimit
|
||||
}
|
||||
|
||||
if o.domain == "" {
|
||||
var system v1alpha1.Terminus
|
||||
err := userClient.Get(ctx, types.NamespacedName{Name: systemObjectName}, &system)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get system info: %v", err)
|
||||
}
|
||||
o.domain = system.Spec.Settings[systemObjectDomainKey]
|
||||
}
|
||||
|
||||
var userList iamv1alpha2.UserList
|
||||
err = userClient.List(ctx, &userList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list users to set creator: %w", err)
|
||||
}
|
||||
var owners []iamv1alpha2.User
|
||||
for _, user := range userList.Items {
|
||||
if role, ok := user.Annotations[annotationKeyRole]; ok && role == roleOwner {
|
||||
owners = append(owners, user)
|
||||
}
|
||||
}
|
||||
if len(owners) > 1 {
|
||||
fmt.Printf("Warning: multiple owners found\n")
|
||||
}
|
||||
if o.role == roleOwner && len(owners) > 0 {
|
||||
return fmt.Errorf("an owner '%s' already exists", owners[0].Name)
|
||||
}
|
||||
|
||||
if o.password == "" {
|
||||
password, passwordEncrypted, err := utils.GenerateEncryptedPassword(8)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating password: %v", err)
|
||||
}
|
||||
log.Println("generated initial password:", password)
|
||||
o.password = passwordEncrypted
|
||||
} else {
|
||||
o.password = utils.MD5(o.password + "@Olares2025")
|
||||
}
|
||||
|
||||
olaresName := fmt.Sprintf("%s@%s", o.name, o.domain)
|
||||
|
||||
user := &iamv1alpha2.User{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: iamv1alpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamv1alpha2.ResourceKindUser,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: o.name,
|
||||
Annotations: map[string]string{
|
||||
"bytetrade.io/creator": creatorCLI,
|
||||
annotationKeyRole: o.role,
|
||||
"bytetrade.io/is-ephemeral": "true",
|
||||
"bytetrade.io/terminus-name": olaresName,
|
||||
"bytetrade.io/launcher-auth-policy": "two_factor",
|
||||
"bytetrade.io/launcher-access-level": "1",
|
||||
annotationKeyMemoryLimit: o.memoryLimit,
|
||||
annotationKeyCPULimit: o.cpuLimit,
|
||||
"iam.kubesphere.io/sync-to-lldap": "true",
|
||||
"iam.kubesphere.io/synced-to-lldap": "false",
|
||||
},
|
||||
},
|
||||
Spec: iamv1alpha2.UserSpec{
|
||||
DisplayName: o.displayName,
|
||||
Email: olaresName,
|
||||
InitialPassword: o.password,
|
||||
Description: o.description,
|
||||
},
|
||||
}
|
||||
|
||||
if o.role == roleOwner || o.role == roleAdmin {
|
||||
user.Spec.Groups = append(user.Spec.Groups, lldapGroupAdmin)
|
||||
}
|
||||
|
||||
err = userClient.Create(ctx, user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User '%s' created successfully\n", o.name)
|
||||
return nil
|
||||
}
|
||||
81
cli/cmd/ctl/user/delete.go
Normal file
81
cli/cmd/ctl/user/delete.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
iamv1alpha2 "github.com/beclab/api/iam/v1alpha2"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"log"
|
||||
)
|
||||
|
||||
type deleteUserOptions struct {
|
||||
name string
|
||||
kubeConfig string
|
||||
}
|
||||
|
||||
func NewCmdDeleteUser() *cobra.Command {
|
||||
o := &deleteUserOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete {name}",
|
||||
Short: "delete an existing user",
|
||||
Aliases: []string{"d", "del", "rm", "remove"},
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.name = args[0]
|
||||
if err := o.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *deleteUserOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&o.kubeConfig, "kubeconfig", "", "path to kubeconfig file")
|
||||
}
|
||||
|
||||
func (o *deleteUserOptions) Validate() error {
|
||||
if o.name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *deleteUserOptions) Run() error {
|
||||
ctx := context.Background()
|
||||
userClient, err := newUserClientFromKubeConfig(o.kubeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user iamv1alpha2.User
|
||||
err = userClient.Get(ctx, types.NamespacedName{Name: o.name}, &user)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return fmt.Errorf("user '%s' not found", o.name)
|
||||
}
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if user.Status.State == "Creating" {
|
||||
return fmt.Errorf("user '%s' is under creation", o.name)
|
||||
}
|
||||
|
||||
if role, ok := user.Annotations[annotationKeyRole]; ok && role == roleOwner {
|
||||
return fmt.Errorf("cannot delete user '%s' with role '%s' ", o.name, role)
|
||||
}
|
||||
|
||||
err = userClient.Delete(ctx, &user)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User '%s' deleted successfully\n", o.name)
|
||||
return nil
|
||||
}
|
||||
84
cli/cmd/ctl/user/get.go
Normal file
84
cli/cmd/ctl/user/get.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
iamv1alpha2 "github.com/beclab/api/iam/v1alpha2"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"log"
|
||||
)
|
||||
|
||||
type getUserOptions struct {
|
||||
name string
|
||||
kubeConfig string
|
||||
output string
|
||||
noHeaders bool
|
||||
}
|
||||
|
||||
func NewCmdGetUser() *cobra.Command {
|
||||
o := &getUserOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "get {name}",
|
||||
Short: "get user details",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.name = args[0]
|
||||
if err := o.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *getUserOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&o.kubeConfig, "kubeconfig", "", "path to kubeconfig file")
|
||||
cmd.Flags().StringVarP(&o.output, "output", "o", "table", "output format (table, json)")
|
||||
cmd.Flags().BoolVar(&o.noHeaders, "no-headers", false, "disable headers")
|
||||
}
|
||||
|
||||
func (o *getUserOptions) Validate() error {
|
||||
if o.name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *getUserOptions) Run() error {
|
||||
ctx := context.Background()
|
||||
|
||||
userClient, err := newUserClientFromKubeConfig(o.kubeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user iamv1alpha2.User
|
||||
err = userClient.Get(ctx, types.NamespacedName{Name: o.name}, &user)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return fmt.Errorf("user '%s' not found", o.name)
|
||||
}
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
info := convertUserObjectToUserInfo(user)
|
||||
|
||||
if o.output == "json" {
|
||||
jsonOutput, _ := json.MarshalIndent(info, "", " ")
|
||||
fmt.Println(string(jsonOutput))
|
||||
} else {
|
||||
if !o.noHeaders {
|
||||
printUserTableHeaders()
|
||||
}
|
||||
printUserTableRow(info)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
182
cli/cmd/ctl/user/list.go
Normal file
182
cli/cmd/ctl/user/list.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
iamv1alpha2 "github.com/beclab/api/iam/v1alpha2"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"log"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var sortFuncs = map[string]func(users []iamv1alpha2.User, i, j int) bool{
|
||||
"name": func(users []iamv1alpha2.User, i, j int) bool {
|
||||
return strings.Compare(users[i].Name, users[j].Name) == -1
|
||||
},
|
||||
"role": func(users []iamv1alpha2.User, i, j int) bool {
|
||||
return strings.Compare(users[i].Annotations[annotationKeyRole], users[j].Annotations[annotationKeyRole]) == -1
|
||||
},
|
||||
"create-time": func(users []iamv1alpha2.User, i, j int) bool {
|
||||
return users[i].CreationTimestamp.Before(&users[j].CreationTimestamp)
|
||||
},
|
||||
"memory": func(users []iamv1alpha2.User, i, j int) bool {
|
||||
iMemoryStr, ok := users[i].Annotations[annotationKeyMemoryLimit]
|
||||
if !ok || iMemoryStr == "" {
|
||||
return false
|
||||
}
|
||||
jMemoryStr, ok := users[j].Annotations[annotationKeyMemoryLimit]
|
||||
if !ok || jMemoryStr == "" {
|
||||
return true
|
||||
}
|
||||
iMemory, err := resource.ParseQuantity(iMemoryStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: invalid memory limit '%s' is set on user '%s'\n", iMemoryStr, users[i].Name)
|
||||
return false
|
||||
}
|
||||
jMemory, err := resource.ParseQuantity(jMemoryStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: invalid memory limit '%s' is set on user '%s'\n", jMemoryStr, users[j].Name)
|
||||
return true
|
||||
}
|
||||
return iMemory.Cmp(jMemory) == -1
|
||||
},
|
||||
"cpu": func(users []iamv1alpha2.User, i, j int) bool {
|
||||
iCPUStr, ok := users[i].Annotations[annotationKeyCPULimit]
|
||||
if !ok || iCPUStr == "" {
|
||||
return false
|
||||
}
|
||||
jCPUStr, ok := users[j].Annotations[annotationKeyCPULimit]
|
||||
if !ok || jCPUStr == "" {
|
||||
return true
|
||||
}
|
||||
iCPU, err := resource.ParseQuantity(iCPUStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: invalid cpu limit '%s' is set on user '%s'", iCPUStr, users[i].Name)
|
||||
return false
|
||||
}
|
||||
jCPU, err := resource.ParseQuantity(jCPUStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: invalid cpu limit '%s' is set on user '%s'", jCPUStr, users[j].Name)
|
||||
return true
|
||||
}
|
||||
return iCPU.Cmp(jCPU) == -1
|
||||
},
|
||||
}
|
||||
|
||||
var sortAliases = map[string]sets.Set[string]{
|
||||
"name": sets.New[string]("n", "N", "Name"),
|
||||
"role": sets.New[string]("r", "R", "Role"),
|
||||
"create-time": sets.New[string]("creation", "created", "created-at", "createdat", "createtime"),
|
||||
"cpu": sets.New[string]("c", "C", "CPU"),
|
||||
"memory": sets.New[string]("m", "M", "Memory"),
|
||||
}
|
||||
|
||||
func getSortFunc(sortBy string) func(users []iamv1alpha2.User, i, j int) bool {
|
||||
if f, ok := sortFuncs[sortBy]; ok {
|
||||
return f
|
||||
}
|
||||
for origin, sortAlias := range sortAliases {
|
||||
if sortAlias.Has(sortBy) {
|
||||
return sortFuncs[origin]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type listUsersOptions struct {
|
||||
kubeConfig string
|
||||
output string
|
||||
noHeaders bool
|
||||
sortBys []string
|
||||
reverse bool
|
||||
}
|
||||
|
||||
func NewCmdListUsers() *cobra.Command {
|
||||
o := &listUsersOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls", "l"},
|
||||
Short: "list all users",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := o.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *listUsersOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&o.kubeConfig, "kubeconfig", "", "path to kubeconfig file")
|
||||
cmd.Flags().StringVarP(&o.output, "output", "o", "table", "output format (table, json)")
|
||||
cmd.Flags().BoolVar(&o.noHeaders, "no-headers", false, "disable headers")
|
||||
cmd.Flags().StringSliceVar(&o.sortBys, "sort", []string{}, "sort output order by (name, role, create-time, memory, cpu)")
|
||||
cmd.Flags().BoolVarP(&o.reverse, "reverse", "r", false, "reverse order")
|
||||
}
|
||||
|
||||
func (o *listUsersOptions) Validate() error {
|
||||
for _, sortBy := range o.sortBys {
|
||||
f := getSortFunc(sortBy)
|
||||
if f == nil {
|
||||
return fmt.Errorf("unknown sort option: %s", sortBy)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *listUsersOptions) Run() error {
|
||||
ctx := context.Background()
|
||||
|
||||
userClient, err := newUserClientFromKubeConfig(o.kubeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userList iamv1alpha2.UserList
|
||||
err = userClient.List(ctx, &userList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
|
||||
for _, sortBy := range o.sortBys {
|
||||
sort.SliceStable(userList.Items, func(i, j int) bool {
|
||||
f := getSortFunc(sortBy)
|
||||
if f == nil {
|
||||
log.Fatalf("unkown sort option: %s", sortBy)
|
||||
}
|
||||
return f(userList.Items, i, j)
|
||||
})
|
||||
}
|
||||
|
||||
if o.reverse {
|
||||
slices.Reverse(userList.Items)
|
||||
}
|
||||
|
||||
users := make([]userInfo, 0, len(userList.Items))
|
||||
for _, user := range userList.Items {
|
||||
users = append(users, convertUserObjectToUserInfo(user))
|
||||
}
|
||||
|
||||
if o.output == "json" {
|
||||
jsonOutput, _ := json.MarshalIndent(users, "", " ")
|
||||
fmt.Println(string(jsonOutput))
|
||||
} else {
|
||||
if !o.noHeaders {
|
||||
printUserTableHeaders()
|
||||
}
|
||||
for _, user := range users {
|
||||
printUserTableRow(user)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
cli/cmd/ctl/user/root.go
Normal file
16
cli/cmd/ctl/user/root.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package user
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func NewUserCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "user management operations",
|
||||
}
|
||||
cmd.AddCommand(NewCmdCreateUser())
|
||||
cmd.AddCommand(NewCmdDeleteUser())
|
||||
cmd.AddCommand(NewCmdListUsers())
|
||||
cmd.AddCommand(NewCmdGetUser())
|
||||
// cmd.AddCommand(NewCmdUpdateUserLimits())
|
||||
return cmd
|
||||
}
|
||||
43
cli/cmd/ctl/user/types.go
Normal file
43
cli/cmd/ctl/user/types.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package user
|
||||
|
||||
type resourceLimit struct {
|
||||
memoryLimit string
|
||||
cpuLimit string
|
||||
}
|
||||
|
||||
type userInfo struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Email string `json:"email"`
|
||||
State string `json:"state"`
|
||||
LastLoginTime *int64 `json:"last_login_time"`
|
||||
CreationTimestamp int64 `json:"creation_timestamp"`
|
||||
Avatar string `json:"avatar"`
|
||||
TerminusName string `json:"terminusName"`
|
||||
WizardComplete bool `json:"wizard_complete"`
|
||||
Roles []string `json:"roles"`
|
||||
MemoryLimit string `json:"memory_limit"`
|
||||
CpuLimit string `json:"cpu_limit"`
|
||||
}
|
||||
|
||||
var (
|
||||
annotationKeyRole = "bytetrade.io/owner-role"
|
||||
annotationKeyMemoryLimit = "bytetrade.io/user-memory-limit"
|
||||
annotationKeyCPULimit = "bytetrade.io/user-cpu-limit"
|
||||
|
||||
roleOwner = "owner"
|
||||
roleAdmin = "admin"
|
||||
roleNormal = "normal"
|
||||
|
||||
creatorCLI = "cli"
|
||||
|
||||
lldapGroupAdmin = "lldap_admin"
|
||||
|
||||
defaultMemoryLimit = "3G"
|
||||
defaultCPULimit = "1"
|
||||
|
||||
systemObjectName = "terminus"
|
||||
systemObjectDomainKey = "domainName"
|
||||
)
|
||||
97
cli/cmd/ctl/user/update.go
Normal file
97
cli/cmd/ctl/user/update.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
iamv1alpha2 "github.com/beclab/api/iam/v1alpha2"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"log"
|
||||
)
|
||||
|
||||
type updateUserLimitsOptions struct {
|
||||
name string
|
||||
resourceLimit
|
||||
kubeConfig string
|
||||
}
|
||||
|
||||
func NewCmdUpdateUserLimits() *cobra.Command {
|
||||
o := &updateUserLimitsOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "update-limits {name}",
|
||||
Aliases: []string{"update-limit", "ulimit", "ulimits"},
|
||||
Short: "update user resource limits",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.name = args[0]
|
||||
if err := o.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := o.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *updateUserLimitsOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&o.memoryLimit, "memory-limit", "m", "", "memory limit")
|
||||
cmd.Flags().StringVarP(&o.cpuLimit, "cpu-limit", "c", "", "cpu limit")
|
||||
cmd.Flags().StringVar(&o.kubeConfig, "kubeconfig", "", "path to kubeconfig file")
|
||||
}
|
||||
|
||||
func (o *updateUserLimitsOptions) Validate() error {
|
||||
if o.name == "" {
|
||||
return fmt.Errorf("user name is required")
|
||||
}
|
||||
|
||||
if o.memoryLimit == "" && o.cpuLimit == "" {
|
||||
return fmt.Errorf("one of memory limit or cpu limit is required")
|
||||
}
|
||||
|
||||
if err := validateResourceLimit(o.resourceLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *updateUserLimitsOptions) Run() error {
|
||||
ctx := context.Background()
|
||||
|
||||
userClient, err := newUserClientFromKubeConfig(o.kubeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var user iamv1alpha2.User
|
||||
err = userClient.Get(ctx, types.NamespacedName{Name: o.name}, &user)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return fmt.Errorf("user '%s' not found", o.name)
|
||||
}
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if user.Annotations == nil {
|
||||
user.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
if o.memoryLimit != "" {
|
||||
user.Annotations[annotationKeyMemoryLimit] = o.memoryLimit
|
||||
}
|
||||
if o.cpuLimit != "" {
|
||||
user.Annotations[annotationKeyCPULimit] = o.cpuLimit
|
||||
}
|
||||
|
||||
err = userClient.Update(ctx, &user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User '%s' resource limits updated successfully\n", o.name)
|
||||
return nil
|
||||
}
|
||||
120
cli/cmd/ctl/user/utils.go
Normal file
120
cli/cmd/ctl/user/utils.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytetrade.io/web3os/app-service/api/sys.bytetrade.io/v1alpha1"
|
||||
"fmt"
|
||||
iamv1alpha2 "github.com/beclab/api/iam/v1alpha2"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"os"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newUserClientFromKubeConfig(kubeconfig string) (client.Client, error) {
|
||||
if kubeconfig == "" {
|
||||
kubeconfig = os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
kubeconfig = clientcmd.RecommendedHomeFile
|
||||
}
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
|
||||
if err := iamv1alpha2.AddToScheme(scheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to add user scheme: %w", err)
|
||||
}
|
||||
|
||||
if err := v1alpha1.AddToScheme(scheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to add system scheme: %w", err)
|
||||
}
|
||||
|
||||
userClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user client: %w", err)
|
||||
}
|
||||
return userClient, nil
|
||||
}
|
||||
|
||||
func validateResourceLimit(limit resourceLimit) error {
|
||||
if limit.memoryLimit != "" {
|
||||
memLimit, err := resource.ParseQuantity(limit.memoryLimit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid memory limit: %v", err)
|
||||
}
|
||||
minMemLimit, _ := resource.ParseQuantity(defaultMemoryLimit)
|
||||
if memLimit.Cmp(minMemLimit) < 0 {
|
||||
return fmt.Errorf("invalid memory limit: %s is less than minimum required: %s", memLimit.String(), minMemLimit.String())
|
||||
}
|
||||
}
|
||||
|
||||
if limit.cpuLimit != "" {
|
||||
cpuLimit, err := resource.ParseQuantity(limit.cpuLimit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cpu limit: %v", err)
|
||||
}
|
||||
minCPULimit, _ := resource.ParseQuantity(defaultCPULimit)
|
||||
if cpuLimit.Cmp(minCPULimit) < 0 {
|
||||
return fmt.Errorf("invalid cpu limit: %s is less than minimum required: %s", cpuLimit.String(), minCPULimit.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertUserObjectToUserInfo(user iamv1alpha2.User) userInfo {
|
||||
info := userInfo{
|
||||
UID: string(user.UID),
|
||||
Name: user.Name,
|
||||
DisplayName: user.Spec.DisplayName,
|
||||
Description: user.Spec.Description,
|
||||
Email: user.Spec.Email,
|
||||
State: string(user.Status.State),
|
||||
CreationTimestamp: user.CreationTimestamp.Unix(),
|
||||
}
|
||||
|
||||
if user.Annotations != nil {
|
||||
if role, ok := user.Annotations[annotationKeyRole]; ok {
|
||||
info.Roles = []string{role}
|
||||
}
|
||||
if terminusName, ok := user.Annotations["bytetrade.io/terminus-name"]; ok {
|
||||
info.TerminusName = terminusName
|
||||
}
|
||||
if avatar, ok := user.Annotations["bytetrade.io/avatar"]; ok {
|
||||
info.Avatar = avatar
|
||||
}
|
||||
if memoryLimit, ok := user.Annotations[annotationKeyMemoryLimit]; ok {
|
||||
info.MemoryLimit = memoryLimit
|
||||
}
|
||||
if cpuLimit, ok := user.Annotations[annotationKeyCPULimit]; ok {
|
||||
info.CpuLimit = cpuLimit
|
||||
}
|
||||
}
|
||||
|
||||
if user.Status.LastLoginTime != nil {
|
||||
lastLogin := user.Status.LastLoginTime.Unix()
|
||||
info.LastLoginTime = &lastLogin
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func printUserTableHeaders() {
|
||||
fmt.Printf("%-20s %-10s %-10s %-30s %-10s %-10s %-10s\n", "NAME", "ROLE", "STATE", "CREATE TIME", "ACTIVATED", "MEMORY", "CPU")
|
||||
}
|
||||
|
||||
func printUserTableRow(info userInfo) {
|
||||
role := roleNormal
|
||||
if len(info.Roles) > 0 {
|
||||
role = info.Roles[0]
|
||||
}
|
||||
fmt.Printf("%-20s %-10s %-10s %-30s %-10s %-10s %-10s\n",
|
||||
info.Name, role, info.State, time.Unix(info.CreationTimestamp, 0).Format(time.RFC3339), strconv.FormatBool(info.WizardComplete), info.MemoryLimit, info.CpuLimit)
|
||||
}
|
||||
Reference in New Issue
Block a user