Skip to content

Commit

Permalink
Add support for organizations, ref: #113
Browse files Browse the repository at this point in the history
We had to revert the support of organizations. This commit reverts the
following commits:

05fc33c
8de0c69
628d9c8
86cfe30
  • Loading branch information
brmzkw committed Aug 30, 2023
1 parent 70a8abb commit fb74c5d
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 35 deletions.
51 changes: 34 additions & 17 deletions pkg/koyeb/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ const (
ctx_logs_client
ctx_mapper
ctx_renderer
ctx_organization
)

// 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
Expand All @@ -36,31 +38,46 @@ func SetupCLIContext(cmd *cobra.Command) 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 != "" {
token, err := GetOrganizationToken(apiClient.OrganizationApi, ctx, organization)
if err != nil {
return err
}
ctx = context.WithValue(ctx, koyeb.ContextAccessToken, token)
cmd.SetContext(ctx)
}
return nil
}

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),
Organization: ctx.Value(ctx_organization).(string),
}
}

// 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)
}
}
35 changes: 21 additions & 14 deletions pkg/koyeb/idmapper/idmapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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
}
124 changes: 124 additions & 0 deletions pkg/koyeb/idmapper/organization.go
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 9 additions & 4 deletions pkg/koyeb/koyeb.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var (
forceASCII bool
debugFull bool
debug bool
organization string

loginCmd = &cobra.Command{
Use: "login",
Expand Down Expand Up @@ -75,7 +76,7 @@ func GetRootCommand() *cobra.Command {
return err
}
DetectUpdates()
return SetupCLIContext(cmd)
return SetupCLIContext(cmd, organization)
},
}

Expand All @@ -89,16 +90,19 @@ 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)
rootCmd.AddCommand(completionCmd)

rootCmd.AddCommand(NewOrganizationCmd())
rootCmd.AddCommand(NewSecretCmd())
rootCmd.AddCommand(NewAppCmd())
rootCmd.AddCommand(NewDomainCmd())
Expand Down Expand Up @@ -250,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
}
72 changes: 72 additions & 0 deletions pkg/koyeb/organizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package koyeb

import (
"context"
"fmt"

"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 {
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 <id>`. Finally you can run your commands again, without the --organization flag.",
}
}
return *res.Token.Id, nil
}
Loading

0 comments on commit fb74c5d

Please sign in to comment.