From 05f729d100dbd6a314365c95f52b2fdd8f5058ea Mon Sep 17 00:00:00 2001 From: Julien Castets Date: Wed, 2 Aug 2023 15:10:27 +0200 Subject: [PATCH 1/4] Create `koyeb orga list` and `koyeb orga switch` --- pkg/koyeb/context.go | 35 +++++--- pkg/koyeb/idmapper/idmapper.go | 35 ++++---- pkg/koyeb/idmapper/organization.go | 124 +++++++++++++++++++++++++++++ pkg/koyeb/koyeb.go | 4 +- pkg/koyeb/organizations.go | 59 ++++++++++++++ pkg/koyeb/organizations_list.go | 95 ++++++++++++++++++++++ pkg/koyeb/organizations_switch.go | 32 ++++++++ 7 files changed, 358 insertions(+), 26 deletions(-) create mode 100644 pkg/koyeb/idmapper/organization.go create mode 100644 pkg/koyeb/organizations.go create mode 100644 pkg/koyeb/organizations_list.go create mode 100644 pkg/koyeb/organizations_switch.go diff --git a/pkg/koyeb/context.go b/pkg/koyeb/context.go index cdcc2777..5b86b32c 100644 --- a/pkg/koyeb/context.go +++ b/pkg/koyeb/context.go @@ -19,7 +19,8 @@ const ( ) // SetupCLIContext is called by the root command to setup the context for all subcommands. -func SetupCLIContext(cmd *cobra.Command) error { +// When `organization` is not empty, it should contain the ID of the organization to switch the context to. +func SetupCLIContext(cmd *cobra.Command, organization string) error { apiClient, err := getApiClient() if err != nil { return err @@ -37,6 +38,15 @@ func SetupCLIContext(cmd *cobra.Command) error { ctx = context.WithValue(ctx, ctx_mapper, idmapper.NewMapper(ctx, apiClient)) ctx = context.WithValue(ctx, ctx_renderer, renderer.NewRenderer(outputFormat)) cmd.SetContext(ctx) + + if organization != "" { + token, err := GetOrganizationToken(apiClient.OrganizationApi, ctx, organization) + if err != nil { + return err + } + ctx = context.WithValue(ctx, koyeb.ContextAccessToken, token) + cmd.SetContext(ctx) + } return nil } @@ -49,18 +59,21 @@ type CLIContext struct { Renderer renderer.Renderer } +// GetCLIContext transforms the untyped context passed to cobra commands into a CLIContext. +func GetCLIContext(ctx context.Context) *CLIContext { + return &CLIContext{ + Context: ctx, + Client: ctx.Value(ctx_client).(*koyeb.APIClient), + LogsClient: ctx.Value(ctx_logs_client).(*LogsAPIClient), + Mapper: ctx.Value(ctx_mapper).(*idmapper.Mapper), + Token: ctx.Value(koyeb.ContextAccessToken).(string), + Renderer: ctx.Value(ctx_renderer).(renderer.Renderer), + } +} + // WithCLIContext is a decorator that provides a CLIContext to cobra commands. func WithCLIContext(fn func(ctx *CLIContext, cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - cliContext := CLIContext{ - Context: ctx, - Client: ctx.Value(ctx_client).(*koyeb.APIClient), - LogsClient: ctx.Value(ctx_logs_client).(*LogsAPIClient), - Mapper: ctx.Value(ctx_mapper).(*idmapper.Mapper), - Token: ctx.Value(koyeb.ContextAccessToken).(string), - Renderer: ctx.Value(ctx_renderer).(renderer.Renderer), - } - return fn(&cliContext, cmd, args) + return fn(GetCLIContext(cmd.Context()), cmd, args) } } diff --git a/pkg/koyeb/idmapper/idmapper.go b/pkg/koyeb/idmapper/idmapper.go index b191014b..6c416382 100644 --- a/pkg/koyeb/idmapper/idmapper.go +++ b/pkg/koyeb/idmapper/idmapper.go @@ -7,13 +7,14 @@ import ( ) type Mapper struct { - app *AppMapper - domain *DomainMapper - service *ServiceMapper - deployment *DeploymentMapper - regional *RegionalDeploymentMapper - instance *InstanceMapper - secret *SecretMapper + app *AppMapper + domain *DomainMapper + service *ServiceMapper + deployment *DeploymentMapper + regional *RegionalDeploymentMapper + instance *InstanceMapper + secret *SecretMapper + organization *OrganizationMapper } func NewMapper(ctx context.Context, client *koyeb.APIClient) *Mapper { @@ -24,15 +25,17 @@ func NewMapper(ctx context.Context, client *koyeb.APIClient) *Mapper { regionalMapper := NewRegionalDeploymentMapper(ctx, client) instanceMapper := NewInstanceMapper(ctx, client) secretMapper := NewSecretMapper(ctx, client) + organizationMapper := NewOrganizationMapper(ctx, client) return &Mapper{ - app: appMapper, - domain: domainMapper, - service: serviceMapper, - deployment: deploymentMapper, - regional: regionalMapper, - instance: instanceMapper, - secret: secretMapper, + app: appMapper, + domain: domainMapper, + service: serviceMapper, + deployment: deploymentMapper, + regional: regionalMapper, + instance: instanceMapper, + secret: secretMapper, + organization: organizationMapper, } } @@ -63,3 +66,7 @@ func (mapper *Mapper) Instance() *InstanceMapper { func (mapper *Mapper) Secret() *SecretMapper { return mapper.secret } + +func (mapper *Mapper) Organization() *OrganizationMapper { + return mapper.organization +} diff --git a/pkg/koyeb/idmapper/organization.go b/pkg/koyeb/idmapper/organization.go new file mode 100644 index 00000000..54726e32 --- /dev/null +++ b/pkg/koyeb/idmapper/organization.go @@ -0,0 +1,124 @@ +package idmapper + +import ( + "context" + "strconv" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" +) + +type OrganizationMapper struct { + ctx context.Context + client *koyeb.APIClient + fetched bool + sidMap *IDMap + nameMap *IDMap +} + +func NewOrganizationMapper(ctx context.Context, client *koyeb.APIClient) *OrganizationMapper { + return &OrganizationMapper{ + ctx: ctx, + client: client, + fetched: false, + sidMap: NewIDMap(), + nameMap: NewIDMap(), + } +} + +func (mapper *OrganizationMapper) ResolveID(val string) (string, error) { + if IsUUIDv4(val) { + return val, nil + } + + if !mapper.fetched { + err := mapper.fetch() + if err != nil { + return "", err + } + } + + id, ok := mapper.sidMap.GetID(val) + if ok { + return id, nil + } + + id, ok = mapper.nameMap.GetID(val) + if ok { + return id, nil + } + + return "", errors.NewCLIErrorForMapperResolve( + "organization", + val, + []string{"organization full UUID", "organization short ID (8 characters)", "organization name"}, + ) +} + +func (mapper *OrganizationMapper) getCurrentUserId() (string, error) { + res, resp, err := mapper.client.ProfileApi.GetCurrentUser(mapper.ctx).Execute() + if err != nil { + return "", errors.NewCLIErrorFromAPIError("Your authentication token is not linked to a user", err, resp) + } + return *res.GetUser().Id, nil +} + +func (mapper *OrganizationMapper) fetch() error { + radix := NewRadixTree() + + userId, err := mapper.getCurrentUserId() + if err != nil { + return err + } + + page := int64(0) + offset := int64(0) + limit := int64(100) + for { + res, resp, err := mapper.client.OrganizationMembersApi. + ListOrganizationMembers(mapper.ctx). + UserId(userId). + Limit(strconv.FormatInt(limit, 10)). + Offset(strconv.FormatInt(offset, 10)). + Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + "Error listing organizations to resolve the provided identifier to an object ID", + err, + resp, + ) + } + + members := res.GetMembers() + for i := range members { + member := &members[i] + radix.Insert(getKey(member.Organization.GetId()), member) + } + + page++ + offset = page * limit + if offset >= res.GetCount() { + break + } + } + + minLength := radix.MinimalLength(8) + err = radix.ForEach(func(key Key, value Value) error { + member := value.(*koyeb.OrganizationMember) + id := member.Organization.GetId() + name := member.Organization.GetName() + sid := getShortID(id, minLength) + + mapper.sidMap.Set(id, sid) + mapper.nameMap.Set(id, name) + + return nil + }) + if err != nil { + return err + } + + mapper.fetched = true + + return nil +} diff --git a/pkg/koyeb/koyeb.go b/pkg/koyeb/koyeb.go index d172d413..e1e4cc53 100644 --- a/pkg/koyeb/koyeb.go +++ b/pkg/koyeb/koyeb.go @@ -75,7 +75,8 @@ func GetRootCommand() *cobra.Command { return err } DetectUpdates() - return SetupCLIContext(cmd) + organization := viper.GetString("organization") + return SetupCLIContext(cmd, organization) }, } @@ -99,6 +100,7 @@ func GetRootCommand() *cobra.Command { rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(completionCmd) + rootCmd.AddCommand(NewOrganizationCmd()) rootCmd.AddCommand(NewSecretCmd()) rootCmd.AddCommand(NewAppCmd()) rootCmd.AddCommand(NewDomainCmd()) diff --git a/pkg/koyeb/organizations.go b/pkg/koyeb/organizations.go new file mode 100644 index 00000000..5fd97784 --- /dev/null +++ b/pkg/koyeb/organizations.go @@ -0,0 +1,59 @@ +package koyeb + +import ( + "context" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + "github.com/spf13/cobra" +) + +func NewOrganizationCmd() *cobra.Command { + h := NewOrganizationHandler() + rootCmd := &cobra.Command{ + Use: "organizations ACTION", + Aliases: []string{"organizations", "organization", "orgas", "orga", "orgs", "org", "organisations", "organisation"}, + Short: "Organization", + } + listCmd := &cobra.Command{ + Use: "list", + Short: "List organizations", + RunE: WithCLIContext(h.List), + } + rootCmd.AddCommand(listCmd) + + switchCmd := &cobra.Command{ + Use: "switch", + Short: "Switch the CLI context to another organization", + RunE: WithCLIContext(h.Switch), + } + rootCmd.AddCommand(switchCmd) + return rootCmd +} + +func NewOrganizationHandler() *OrganizationHandler { + return &OrganizationHandler{} +} + +type OrganizationHandler struct { +} + +func ResolveOrganizationArgs(ctx *CLIContext, val string) (string, error) { + organizationMapper := ctx.Mapper.Organization() + id, err := organizationMapper.ResolveID(val) + if err != nil { + return "", err + } + return id, nil +} + +// GetOrganizationToken calls /v1/organizations/{organizationId}/switch which returns a token to access the resources of organizationId +func GetOrganizationToken(api koyeb.OrganizationApi, ctx context.Context, organizationId string) (string, error) { + //SwitchOrganization requires to pass an empty body + body := make(map[string]interface{}) + res, resp, err := api.SwitchOrganization(ctx, organizationId).Body(body).Execute() + if err != nil { + return "", errors.NewCLIErrorFromAPIError("unable to switch the current organization", err, resp) + } + return *res.Token.Id, nil +} diff --git a/pkg/koyeb/organizations_list.go b/pkg/koyeb/organizations_list.go new file mode 100644 index 00000000..32f0e3c1 --- /dev/null +++ b/pkg/koyeb/organizations_list.go @@ -0,0 +1,95 @@ +package koyeb + +import ( + "strconv" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + "github.com/koyeb/koyeb-cli/pkg/koyeb/idmapper" + "github.com/koyeb/koyeb-cli/pkg/koyeb/renderer" + "github.com/spf13/cobra" +) + +func getCurrentUserId(ctx *CLIContext) (string, error) { + res, resp, err := ctx.Client.ProfileApi.GetCurrentUser(ctx.Context).Execute() + if err != nil { + return "", errors.NewCLIErrorFromAPIError("The token used is not linked to a user", err, resp) + } + return *res.GetUser().Id, nil +} + +func (h *OrganizationHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) error { + userId, err := getCurrentUserId(ctx) + if err != nil { + return err + } + + list := []koyeb.OrganizationMember{} + page := int64(0) + offset := int64(0) + limit := int64(100) + + for { + res, resp, err := ctx.Client.OrganizationMembersApi. + ListOrganizationMembers(ctx.Context). + UserId(userId). + Limit(strconv.FormatInt(limit, 10)).Offset(strconv.FormatInt(offset, 10)). + Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError("Error while listing organizations", err, resp) + } + list = append(list, res.GetMembers()...) + + page++ + offset = page * limit + if offset >= res.GetCount() { + break + } + } + + full := GetBoolFlags(cmd, "full") + reply := NewListOragnizationsReply(ctx.Mapper, &koyeb.ListOrganizationMembersReply{Members: list}, full) + ctx.Renderer.Render(reply) + return nil +} + +type ListOrganizationsReply struct { + mapper *idmapper.Mapper + value *koyeb.ListOrganizationMembersReply + full bool +} + +func NewListOragnizationsReply(mapper *idmapper.Mapper, value *koyeb.ListOrganizationMembersReply, full bool) *ListOrganizationsReply { + return &ListOrganizationsReply{ + mapper: mapper, + value: value, + full: full, + } +} + +func (ListOrganizationsReply) Title() string { + return "Organizations" +} + +func (r *ListOrganizationsReply) MarshalBinary() ([]byte, error) { + return r.value.MarshalJSON() +} + +func (r *ListOrganizationsReply) Headers() []string { + return []string{"id", "name", "plan"} +} + +func (r *ListOrganizationsReply) Fields() []map[string]string { + items := r.value.GetMembers() + resp := make([]map[string]string, 0, len(items)) + + for _, member := range items { + fields := map[string]string{ + "id": renderer.FormatID(member.Organization.GetId(), r.full), + "name": member.Organization.GetName(), + "plan": string(member.Organization.GetPlan()), + } + resp = append(resp, fields) + } + return resp +} diff --git a/pkg/koyeb/organizations_switch.go b/pkg/koyeb/organizations_switch.go new file mode 100644 index 00000000..8a559c5c --- /dev/null +++ b/pkg/koyeb/organizations_switch.go @@ -0,0 +1,32 @@ +package koyeb + +import ( + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func (h *OrganizationHandler) Switch(ctx *CLIContext, cmd *cobra.Command, args []string) error { + organization, err := ResolveOrganizationArgs(ctx, args[0]) + if err != nil { + return err + } + viper.Set("organization", organization) + + err = viper.WriteConfig() + if err != nil { + return &errors.CLIError{ + What: "Unable to switch the current organization", + Why: "we were unable to write the configuration file", + Additional: []string{ + "The command `koyeb organization switch` needs to update your configuration file, usually located in $HOME/.koyeb.yaml", + "If you do not have write access to this file, you can use the --config flag to specify a different location.", + "Alternatively, you can manually edit the configuration file and set the organization field to the organization ID you want to use.", + "You can also provide the organization UUID with the --organization flag.", + }, + Orig: err, + Solution: "Fix the issue preventing the CLI to write the configuration file, or manually edit the configuration file", + } + } + return nil +} From df501cc3448ca9bc66fcb9bbbfe44a8495a54abd Mon Sep 17 00:00:00 2001 From: Julien Castets Date: Wed, 2 Aug 2023 15:43:15 +0200 Subject: [PATCH 2/4] =?UTF-8?q?orga=20list:=20display=20=E2=9C=93=20for=20?= =?UTF-8?q?the=20current=20organization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/koyeb/context.go | 28 ++++++++++++++++------------ pkg/koyeb/organizations_list.go | 31 +++++++++++++++++++------------ 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/pkg/koyeb/context.go b/pkg/koyeb/context.go index 5b86b32c..339143c9 100644 --- a/pkg/koyeb/context.go +++ b/pkg/koyeb/context.go @@ -16,6 +16,7 @@ const ( ctx_logs_client ctx_mapper ctx_renderer + ctx_organization ) // SetupCLIContext is called by the root command to setup the context for all subcommands. @@ -37,6 +38,7 @@ func SetupCLIContext(cmd *cobra.Command, organization string) error { ctx = context.WithValue(ctx, ctx_logs_client, logsApiClient) ctx = context.WithValue(ctx, ctx_mapper, idmapper.NewMapper(ctx, apiClient)) ctx = context.WithValue(ctx, ctx_renderer, renderer.NewRenderer(outputFormat)) + ctx = context.WithValue(ctx, ctx_organization, organization) cmd.SetContext(ctx) if organization != "" { @@ -51,23 +53,25 @@ func SetupCLIContext(cmd *cobra.Command, organization string) error { } type CLIContext struct { - Context context.Context - Client *koyeb.APIClient - LogsClient *LogsAPIClient - Mapper *idmapper.Mapper - Token string - Renderer renderer.Renderer + Context context.Context + Client *koyeb.APIClient + LogsClient *LogsAPIClient + Mapper *idmapper.Mapper + Token string + Renderer renderer.Renderer + Organization string } // GetCLIContext transforms the untyped context passed to cobra commands into a CLIContext. func GetCLIContext(ctx context.Context) *CLIContext { return &CLIContext{ - Context: ctx, - Client: ctx.Value(ctx_client).(*koyeb.APIClient), - LogsClient: ctx.Value(ctx_logs_client).(*LogsAPIClient), - Mapper: ctx.Value(ctx_mapper).(*idmapper.Mapper), - Token: ctx.Value(koyeb.ContextAccessToken).(string), - Renderer: ctx.Value(ctx_renderer).(renderer.Renderer), + Context: ctx, + Client: ctx.Value(ctx_client).(*koyeb.APIClient), + LogsClient: ctx.Value(ctx_logs_client).(*LogsAPIClient), + Mapper: ctx.Value(ctx_mapper).(*idmapper.Mapper), + Token: ctx.Value(koyeb.ContextAccessToken).(string), + Renderer: ctx.Value(ctx_renderer).(renderer.Renderer), + Organization: ctx.Value(ctx_organization).(string), } } diff --git a/pkg/koyeb/organizations_list.go b/pkg/koyeb/organizations_list.go index 32f0e3c1..9014f6ae 100644 --- a/pkg/koyeb/organizations_list.go +++ b/pkg/koyeb/organizations_list.go @@ -48,22 +48,24 @@ func (h *OrganizationHandler) List(ctx *CLIContext, cmd *cobra.Command, args []s } full := GetBoolFlags(cmd, "full") - reply := NewListOragnizationsReply(ctx.Mapper, &koyeb.ListOrganizationMembersReply{Members: list}, full) + reply := NewListOragnizationsReply(ctx.Mapper, &koyeb.ListOrganizationMembersReply{Members: list}, full, ctx.Organization) ctx.Renderer.Render(reply) return nil } type ListOrganizationsReply struct { - mapper *idmapper.Mapper - value *koyeb.ListOrganizationMembersReply - full bool + mapper *idmapper.Mapper + value *koyeb.ListOrganizationMembersReply + full bool + currentOrganization string } -func NewListOragnizationsReply(mapper *idmapper.Mapper, value *koyeb.ListOrganizationMembersReply, full bool) *ListOrganizationsReply { +func NewListOragnizationsReply(mapper *idmapper.Mapper, value *koyeb.ListOrganizationMembersReply, full bool, currentOrganization string) *ListOrganizationsReply { return &ListOrganizationsReply{ - mapper: mapper, - value: value, - full: full, + mapper: mapper, + value: value, + full: full, + currentOrganization: currentOrganization, } } @@ -76,7 +78,7 @@ func (r *ListOrganizationsReply) MarshalBinary() ([]byte, error) { } func (r *ListOrganizationsReply) Headers() []string { - return []string{"id", "name", "plan"} + return []string{"id", "name", "plan", "current"} } func (r *ListOrganizationsReply) Fields() []map[string]string { @@ -84,10 +86,15 @@ func (r *ListOrganizationsReply) Fields() []map[string]string { resp := make([]map[string]string, 0, len(items)) for _, member := range items { + current := "" + if member.Organization.GetId() == r.currentOrganization { + current = "✓" + } fields := map[string]string{ - "id": renderer.FormatID(member.Organization.GetId(), r.full), - "name": member.Organization.GetName(), - "plan": string(member.Organization.GetPlan()), + "id": renderer.FormatID(member.Organization.GetId(), r.full), + "name": member.Organization.GetName(), + "plan": string(member.Organization.GetPlan()), + "current": current, } resp = append(resp, fields) } From db955169541f82f0a2c3e29dffdcb7856fa41c57 Mon Sep 17 00:00:00 2001 From: Julien Castets Date: Wed, 2 Aug 2023 16:14:43 +0200 Subject: [PATCH 3/4] Add --organization and fix list when the flag is not set --- pkg/koyeb/koyeb.go | 11 +++++++---- pkg/koyeb/organizations_list.go | 13 ++++++++++++- pkg/koyeb/organizations_switch.go | 4 +--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pkg/koyeb/koyeb.go b/pkg/koyeb/koyeb.go index e1e4cc53..30549435 100644 --- a/pkg/koyeb/koyeb.go +++ b/pkg/koyeb/koyeb.go @@ -30,6 +30,7 @@ var ( forceASCII bool debugFull bool debug bool + organization string loginCmd = &cobra.Command{ Use: "login", @@ -75,7 +76,6 @@ func GetRootCommand() *cobra.Command { return err } DetectUpdates() - organization := viper.GetString("organization") return SetupCLIContext(cmd, organization) }, } @@ -90,11 +90,13 @@ func GetRootCommand() *cobra.Command { rootCmd.PersistentFlags().BoolP("full", "", false, "do not truncate output") rootCmd.PersistentFlags().String("url", "https://app.koyeb.com", "url of the api") rootCmd.PersistentFlags().String("token", "", "API token") + rootCmd.PersistentFlags().StringVar(&organization, "organization", "", "organization ID") // viper.BindPFlag returns an error only if the second argument is nil, which is never the case here, so we ignore the error - viper.BindPFlag("url", rootCmd.PersistentFlags().Lookup("url")) //nolint:errcheck - viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")) //nolint:errcheck - viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) //nolint:errcheck + viper.BindPFlag("url", rootCmd.PersistentFlags().Lookup("url")) //nolint:errcheck + viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")) //nolint:errcheck + viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) //nolint:errcheck + viper.BindPFlag("organization", rootCmd.PersistentFlags().Lookup("organization")) //nolint:errcheck rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(versionCmd) @@ -252,5 +254,6 @@ func initConfig(rootCmd *cobra.Command) error { apiurl = viper.GetString("url") token = viper.GetString("token") debug = viper.GetBool("debug") + organization = viper.GetString("organization") return nil } diff --git a/pkg/koyeb/organizations_list.go b/pkg/koyeb/organizations_list.go index 9014f6ae..e4eb3a24 100644 --- a/pkg/koyeb/organizations_list.go +++ b/pkg/koyeb/organizations_list.go @@ -47,8 +47,19 @@ func (h *OrganizationHandler) List(ctx *CLIContext, cmd *cobra.Command, args []s } } + // ctx.Organization is empty when the field "organization" is not set in the + // configuration file, and is not provided with the --organization flag. + currentOrganization := ctx.Organization + if currentOrganization == "" { + res, resp, err := ctx.Client.ProfileApi.GetCurrentOrganization(ctx.Context).Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError("Unable to fetch the current organization", err, resp) + } + currentOrganization = *res.Organization.Id + } + full := GetBoolFlags(cmd, "full") - reply := NewListOragnizationsReply(ctx.Mapper, &koyeb.ListOrganizationMembersReply{Members: list}, full, ctx.Organization) + reply := NewListOragnizationsReply(ctx.Mapper, &koyeb.ListOrganizationMembersReply{Members: list}, full, currentOrganization) ctx.Renderer.Render(reply) return nil } diff --git a/pkg/koyeb/organizations_switch.go b/pkg/koyeb/organizations_switch.go index 8a559c5c..02ec486f 100644 --- a/pkg/koyeb/organizations_switch.go +++ b/pkg/koyeb/organizations_switch.go @@ -12,9 +12,7 @@ func (h *OrganizationHandler) Switch(ctx *CLIContext, cmd *cobra.Command, args [ return err } viper.Set("organization", organization) - - err = viper.WriteConfig() - if err != nil { + if err := viper.WriteConfig(); err != nil { return &errors.CLIError{ What: "Unable to switch the current organization", Why: "we were unable to write the configuration file", From b7fe71baa8a637ac281f20b367a14726fd58cbc7 Mon Sep 17 00:00:00 2001 From: Julien Castets Date: Wed, 2 Aug 2023 16:33:12 +0200 Subject: [PATCH 4/4] Better error message when --organization is invalid --- pkg/koyeb/organizations.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/koyeb/organizations.go b/pkg/koyeb/organizations.go index 5fd97784..dc639efb 100644 --- a/pkg/koyeb/organizations.go +++ b/pkg/koyeb/organizations.go @@ -2,6 +2,7 @@ package koyeb import ( "context" + "fmt" "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" @@ -53,7 +54,19 @@ func GetOrganizationToken(api koyeb.OrganizationApi, ctx context.Context, organi body := make(map[string]interface{}) res, resp, err := api.SwitchOrganization(ctx, organizationId).Body(body).Execute() if err != nil { - return "", errors.NewCLIErrorFromAPIError("unable to switch the current organization", err, resp) + errBuf := make([]byte, 1024) + // if the body can't be read, it won't be displayed in the error below + resp.Body.Read(errBuf) // nolint:errcheck + return "", &errors.CLIError{ + What: "Error while switching the current organization", + Why: fmt.Sprintf("the API endpoint which switches the current organization returned an error %d", resp.StatusCode), + Additional: []string{ + "You provided an organization id with the --organization flag, or the `organization` field is set in your configuration file.", + "The value provided is likely incorrect, or you don't have access to this organization.", + }, + Orig: fmt.Errorf("HTTP/%d\n\n%s", resp.StatusCode, errBuf), + Solution: "List your organizations with `koyeb --organization=\"\" organization list`, then switch to the organization you want to use with `koyeb --organization=\"\" organization switch `. Finally you can run your commands again, without the --organization flag.", + } } return *res.Token.Id, nil }