Skip to content

Commit

Permalink
Create koyeb orga list and koyeb orga switch
Browse files Browse the repository at this point in the history
  • Loading branch information
brmzkw committed Aug 2, 2023
1 parent 16ddd8e commit ab6d584
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 26 deletions.
35 changes: 24 additions & 11 deletions pkg/koyeb/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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)
}
}
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
}
4 changes: 3 additions & 1 deletion pkg/koyeb/koyeb.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func GetRootCommand() *cobra.Command {
return err
}
DetectUpdates()
return SetupCLIContext(cmd)
organization := viper.GetString("organization")
return SetupCLIContext(cmd, organization)
},
}

Expand All @@ -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())
Expand Down
59 changes: 59 additions & 0 deletions pkg/koyeb/organizations.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit ab6d584

Please sign in to comment.