diff --git a/pkg/koyeb/apps.go b/pkg/koyeb/apps.go index 67832a50..ab69c8d4 100644 --- a/pkg/koyeb/apps.go +++ b/pkg/koyeb/apps.go @@ -80,6 +80,7 @@ func NewAppCmd() *cobra.Command { Short: "List apps", RunE: WithCLIContext(h.List), } + listAppCmd.Flags().Bool("all-projects", false, "List apps from all projects") appCmd.AddCommand(listAppCmd) describeAppCmd := &cobra.Command{ diff --git a/pkg/koyeb/apps_create.go b/pkg/koyeb/apps_create.go index 4ddf5a9c..23b048c9 100644 --- a/pkg/koyeb/apps_create.go +++ b/pkg/koyeb/apps_create.go @@ -22,6 +22,7 @@ func (h *AppHandler) CreateApp(ctx *CLIContext, payload *koyeb.CreateApp) (*koye func (h *AppHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createApp *koyeb.CreateApp) error { createApp.SetName(args[0]) + applyProjectID(createApp, ctx.Project) res, err := h.CreateApp(ctx, createApp) if err != nil { diff --git a/pkg/koyeb/apps_init.go b/pkg/koyeb/apps_init.go index 5f18a53e..e878ca28 100644 --- a/pkg/koyeb/apps_init.go +++ b/pkg/koyeb/apps_init.go @@ -22,6 +22,7 @@ func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, cr uid := uuid.Must(uuid.NewV4()) createService.SetAppId(uid.String()) + applyProjectID(createService, ctx.Project) _, resp, err := ctx.Client.ServicesApi.CreateService(ctx.Context).DryRun(true).Service(*createService).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( @@ -32,6 +33,7 @@ func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, cr } createApp.SetName(args[0]) + applyProjectID(createApp, ctx.Project) res, resp, err := ctx.Client.AppsApi.CreateApp(ctx.Context).App(*createApp).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( diff --git a/pkg/koyeb/apps_list.go b/pkg/koyeb/apps_list.go index 6cad0b9e..fbe83b1f 100644 --- a/pkg/koyeb/apps_list.go +++ b/pkg/koyeb/apps_list.go @@ -17,8 +17,13 @@ func (h *AppHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) er offset := int64(0) limit := int64(100) for { - res, resp, err := ctx.Client.AppsApi.ListApps(ctx.Context). - Limit(strconv.FormatInt(limit, 10)).Offset(strconv.FormatInt(offset, 10)).Execute() + req := ctx.Client.AppsApi.ListApps(ctx.Context). + Limit(strconv.FormatInt(limit, 10)). + Offset(strconv.FormatInt(offset, 10)) + if ctx.Project != "" { + req = req.ProjectId(ctx.Project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error while listing the applications", diff --git a/pkg/koyeb/archives_create.go b/pkg/koyeb/archives_create.go index ed009b26..0ecd04ae 100644 --- a/pkg/koyeb/archives_create.go +++ b/pkg/koyeb/archives_create.go @@ -46,6 +46,7 @@ func (h *ArchiveHandler) CreateArchive(ctx *CLIContext, path string) (*koyeb.Cre // because the underlying type to store the size is uint64, which is not // representable in JSON. c.SetSize(fmt.Sprintf("%d", stat.Size())) + applyProjectID(c, ctx.Project) res, resp, err := ctx.Client.ArchivesApi.CreateArchive(ctx.Context).Archive(*c).Execute() if err != nil { diff --git a/pkg/koyeb/context.go b/pkg/koyeb/context.go index 85f47289..83bee3ed 100644 --- a/pkg/koyeb/context.go +++ b/pkg/koyeb/context.go @@ -18,11 +18,20 @@ const ( ctx_mapper ctx_renderer ctx_organization + ctx_project ) +func topLevelCommand(cmd *cobra.Command) *cobra.Command { + current := cmd + for current.Parent() != nil && current.Parent().Parent() != nil { + current = current.Parent() + } + return current +} + // SetupCLIContext is called by the root command to setup the context for all subcommands. // 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 { +func SetupCLIContext(cmd *cobra.Command, organization string, project string) error { apiClient, err := getApiClient() if err != nil { return err @@ -43,6 +52,39 @@ func SetupCLIContext(cmd *cobra.Command, organization string) error { cmd.SetContext(ctx) } + activeProject := "" + topLevel := topLevelCommand(cmd) + if flag := cmd.Flags().Lookup("all-projects"); flag != nil { + allProjects, err := cmd.Flags().GetBool("all-projects") + if err != nil { + return err + } + if allProjects { + project = "" + } + } + if topLevel.Name() == "organizations" || topLevel.Name() == "projects" { + project = "" + } + if project == "" { + project, err = getOrganizationDefaultProjectID(&CLIContext{ + Context: ctx, + Client: apiClient, + Token: ctx.Value(koyeb.ContextAccessToken).(string), + Organization: organization, + }) + if err != nil { + return err + } + } + if project != "" { + projectMapper := idmapper.NewProjectMapper(ctx, apiClient) + activeProject, err = projectMapper.ResolveID(project) + if err != nil { + return err + } + } + ctx = context.WithValue(ctx, ctx_client, apiClient) logsApiClient, err := NewLogsAPIClient(apiClient, apiurl, ctx.Value(koyeb.ContextAccessToken).(string)) @@ -57,9 +99,10 @@ func SetupCLIContext(cmd *cobra.Command, organization string) error { } ctx = context.WithValue(ctx, ctx_exec_client, execApiClient) - ctx = context.WithValue(ctx, ctx_mapper, idmapper.NewMapper(ctx, apiClient)) + ctx = context.WithValue(ctx, ctx_mapper, idmapper.NewMapper(ctx, apiClient, activeProject)) ctx = context.WithValue(ctx, ctx_renderer, renderer.NewRenderer(outputFormat)) ctx = context.WithValue(ctx, ctx_organization, organization) + ctx = context.WithValue(ctx, ctx_project, activeProject) cmd.SetContext(ctx) return nil @@ -74,6 +117,7 @@ type CLIContext struct { Token string Renderer renderer.Renderer Organization string + Project string } // GetCLIContext transforms the untyped context passed to cobra commands into a CLIContext. @@ -87,6 +131,7 @@ func GetCLIContext(ctx context.Context) *CLIContext { Token: ctx.Value(koyeb.ContextAccessToken).(string), Renderer: ctx.Value(ctx_renderer).(renderer.Renderer), Organization: ctx.Value(ctx_organization).(string), + Project: ctx.Value(ctx_project).(string), } } diff --git a/pkg/koyeb/databases_create.go b/pkg/koyeb/databases_create.go index 4f86db59..7b82128e 100644 --- a/pkg/koyeb/databases_create.go +++ b/pkg/koyeb/databases_create.go @@ -15,6 +15,7 @@ import ( func TryCreateKoyebApplication(name string, ctx *CLIContext) error { createApp := koyeb.NewCreateAppWithDefaults() createApp.SetName(name) + applyProjectID(createApp, ctx.Project) _, resp, err := ctx.Client.AppsApi.CreateApp(ctx.Context).App(*createApp).Execute() if err != nil { @@ -56,6 +57,7 @@ func (h *DatabaseHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []str } createService.SetAppId(appID) + applyProjectID(createService, ctx.Project) res, resp, err := ctx.Client.ServicesApi.CreateService(ctx.Context).Service(*createService).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( diff --git a/pkg/koyeb/databases_list.go b/pkg/koyeb/databases_list.go index 77f162a4..84b50a12 100644 --- a/pkg/koyeb/databases_list.go +++ b/pkg/koyeb/databases_list.go @@ -25,11 +25,14 @@ func (h *DatabaseHandler) List(ctx *CLIContext, cmd *cobra.Command, args []strin offset := int64(0) limit := int64(100) for { - res, resp, err := ctx.Client.ServicesApi.ListServices(ctx.Context). + req := ctx.Client.ServicesApi.ListServices(ctx.Context). Limit(strconv.FormatInt(limit, 10)). Offset(strconv.FormatInt(offset, 10)). - Types([]string{"DATABASE"}). - Execute() + Types([]string{"DATABASE"}) + if ctx.Project != "" { + req = req.ProjectId(ctx.Project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error while listing database services", diff --git a/pkg/koyeb/domains_create.go b/pkg/koyeb/domains_create.go index 16c09c64..f603ec3b 100644 --- a/pkg/koyeb/domains_create.go +++ b/pkg/koyeb/domains_create.go @@ -12,6 +12,7 @@ func (h *DomainHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []strin createDomainReq := koyeb.NewCreateDomainWithDefaults() createDomainReq.SetName(args[0]) createDomainReq.SetType(koyeb.DOMAINTYPE_CUSTOM) + applyProjectID(createDomainReq, ctx.Project) attachToApp := GetStringFlags(cmd, "attach-to") if attachToApp != "" { diff --git a/pkg/koyeb/domains_list.go b/pkg/koyeb/domains_list.go index 10c2a949..6288f2ff 100644 --- a/pkg/koyeb/domains_list.go +++ b/pkg/koyeb/domains_list.go @@ -17,11 +17,14 @@ func (h *DomainHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) offset := int64(0) limit := int64(100) for { - res, resp, err := ctx.Client.DomainsApi.ListDomains(ctx.Context). + req := ctx.Client.DomainsApi.ListDomains(ctx.Context). Limit(strconv.FormatInt(limit, 10)). Offset(strconv.FormatInt(offset, 10)). - Types([]string{string(koyeb.DOMAINTYPE_CUSTOM)}). - Execute() + Types([]string{string(koyeb.DOMAINTYPE_CUSTOM)}) + if ctx.Project != "" { + req = req.ProjectId(ctx.Project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error while listing the domains", diff --git a/pkg/koyeb/idmapper/app.go b/pkg/koyeb/idmapper/app.go index 2c45986b..5a45a4fb 100644 --- a/pkg/koyeb/idmapper/app.go +++ b/pkg/koyeb/idmapper/app.go @@ -12,16 +12,18 @@ import ( type AppMapper struct { ctx context.Context client *koyeb.APIClient + project string fetched bool sidMap *IDMap nameMap *IDMap autoDomainMap *IDMap } -func NewAppMapper(ctx context.Context, client *koyeb.APIClient) *AppMapper { +func NewAppMapper(ctx context.Context, client *koyeb.APIClient, project string) *AppMapper { return &AppMapper{ ctx: ctx, client: client, + project: project, fetched: false, sidMap: NewIDMap(), nameMap: NewIDMap(), @@ -67,7 +69,16 @@ func (mapper *AppMapper) GetName(id string) (string, error) { name, ok := mapper.nameMap.GetValue(id) if !ok { - return "", fmt.Errorf("app name not found for %q", id) + res, resp, err := mapper.client.AppsApi.GetApp(mapper.ctx, id).Execute() + if err != nil { + return "", errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error retrieving the application %q", id), + err, + resp, + ) + } + app := res.GetApp() + return app.GetName(), nil } return name, nil @@ -83,6 +94,27 @@ func (mapper *AppMapper) GetAutoDomain(id string) (string, error) { name, ok := mapper.autoDomainMap.GetValue(id) if !ok { + res, resp, err := mapper.client.AppsApi.GetApp(mapper.ctx, id).Execute() + if err != nil { + return "", errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error retrieving the application %q", id), + err, + resp, + ) + } + app := res.GetApp() + for _, domain := range app.GetDomains() { + if domain.GetType() != koyeb.DOMAINTYPE_AUTOASSIGNED { + continue + } + + if !domain.HasCloudflare() { + continue + } + + return domain.GetId(), nil + } + return "", fmt.Errorf("app automatic domain not found for %q", id) } @@ -97,10 +129,13 @@ func (mapper *AppMapper) fetch() error { limit := int64(100) for { - res, resp, err := mapper.client.AppsApi.ListApps(mapper.ctx). + req := mapper.client.AppsApi.ListApps(mapper.ctx). Limit(strconv.FormatInt(limit, 10)). - Offset(strconv.FormatInt(offset, 10)). - Execute() + Offset(strconv.FormatInt(offset, 10)) + if mapper.project != "" { + req = req.ProjectId(mapper.project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error listing applications to resolve the provided identifier to an object ID", diff --git a/pkg/koyeb/idmapper/domain.go b/pkg/koyeb/idmapper/domain.go index 1ad2048c..e7a85ddb 100644 --- a/pkg/koyeb/idmapper/domain.go +++ b/pkg/koyeb/idmapper/domain.go @@ -12,15 +12,17 @@ import ( type DomainMapper struct { ctx context.Context client *koyeb.APIClient + project string fetched bool sidMap *IDMap nameMap *IDMap } -func NewDomainMapper(ctx context.Context, client *koyeb.APIClient) *DomainMapper { +func NewDomainMapper(ctx context.Context, client *koyeb.APIClient, project string) *DomainMapper { return &DomainMapper{ ctx: ctx, client: client, + project: project, fetched: false, sidMap: NewIDMap(), nameMap: NewIDMap(), @@ -80,10 +82,13 @@ func (mapper *DomainMapper) fetch() error { limit := int64(100) for { - res, resp, err := mapper.client.DomainsApi.ListDomains(mapper.ctx). + req := mapper.client.DomainsApi.ListDomains(mapper.ctx). Limit(strconv.FormatInt(limit, 10)). - Offset(strconv.FormatInt(offset, 10)). - Execute() + Offset(strconv.FormatInt(offset, 10)) + if mapper.project != "" { + req = req.ProjectId(mapper.project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error listing domains to resolve the provided identifier to an object ID", diff --git a/pkg/koyeb/idmapper/idmapper.go b/pkg/koyeb/idmapper/idmapper.go index c4a953a9..205ea809 100644 --- a/pkg/koyeb/idmapper/idmapper.go +++ b/pkg/koyeb/idmapper/idmapper.go @@ -13,6 +13,7 @@ type Mapper struct { deployment *DeploymentMapper regional *RegionalDeploymentMapper instance *InstanceMapper + project *ProjectMapper secret *SecretMapper organization *OrganizationMapper database *DatabaseMapper @@ -20,17 +21,18 @@ type Mapper struct { snapshot *SnapshotMapper } -func NewMapper(ctx context.Context, client *koyeb.APIClient) *Mapper { - appMapper := NewAppMapper(ctx, client) - domainMapper := NewDomainMapper(ctx, client) - serviceMapper := NewServiceMapper(ctx, client, appMapper) +func NewMapper(ctx context.Context, client *koyeb.APIClient, project string) *Mapper { + projectMapper := NewProjectMapper(ctx, client) + appMapper := NewAppMapper(ctx, client, project) + domainMapper := NewDomainMapper(ctx, client, project) + serviceMapper := NewServiceMapper(ctx, client, appMapper, project) deploymentMapper := NewDeploymentMapper(ctx, client) regionalMapper := NewRegionalDeploymentMapper(ctx, client) instanceMapper := NewInstanceMapper(ctx, client) - secretMapper := NewSecretMapper(ctx, client) + secretMapper := NewSecretMapper(ctx, client, project) organizationMapper := NewOrganizationMapper(ctx, client) databaseMapper := NewDatabaseMapper(ctx, client, appMapper) - volumeMapper := NewVolumeMapper(ctx, client) + volumeMapper := NewVolumeMapper(ctx, client, project) snapshotMapper := NewSnapshotMapper(ctx, client) return &Mapper{ @@ -40,6 +42,7 @@ func NewMapper(ctx context.Context, client *koyeb.APIClient) *Mapper { deployment: deploymentMapper, regional: regionalMapper, instance: instanceMapper, + project: projectMapper, secret: secretMapper, organization: organizationMapper, database: databaseMapper, @@ -72,6 +75,10 @@ func (mapper *Mapper) Instance() *InstanceMapper { return mapper.instance } +func (mapper *Mapper) Project() *ProjectMapper { + return mapper.project +} + func (mapper *Mapper) Secret() *SecretMapper { return mapper.secret } diff --git a/pkg/koyeb/idmapper/project.go b/pkg/koyeb/idmapper/project.go new file mode 100644 index 00000000..17cbe295 --- /dev/null +++ b/pkg/koyeb/idmapper/project.go @@ -0,0 +1,125 @@ +package idmapper + +import ( + "context" + "strconv" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" +) + +type ProjectMapper struct { + ctx context.Context + client *koyeb.APIClient + fetched bool + sidMap *IDMap + nameMap *IDMap + names []string +} + +func NewProjectMapper(ctx context.Context, client *koyeb.APIClient) *ProjectMapper { + return &ProjectMapper{ + ctx: ctx, + client: client, + fetched: false, + sidMap: NewIDMap(), + nameMap: NewIDMap(), + names: []string{}, + } +} + +func (mapper *ProjectMapper) ResolveID(val string) (string, error) { + if IsUUIDv4(val) { + return val, nil + } + + if !mapper.fetched { + if err := mapper.fetch(); err != nil { + return "", err + } + } + + if id, ok := mapper.sidMap.GetID(val); ok { + return id, nil + } + + if id, ok := mapper.nameMap.GetID(val); ok { + return id, nil + } + + return "", errors.NewCLIErrorForMapperResolve( + "project", + val, + []string{"project full UUID", "project short ID (8 characters)", "project name"}, + ) +} + +func (mapper *ProjectMapper) Names() ([]string, error) { + if !mapper.fetched { + if err := mapper.fetch(); err != nil { + return nil, err + } + } + + ret := make([]string, len(mapper.names)) + copy(ret, mapper.names) + return ret, nil +} + +func (mapper *ProjectMapper) fetch() error { + radix := NewRadixTree() + + page := int64(0) + offset := int64(0) + limit := int64(100) + for { + res, resp, err := mapper.client.ProjectsApi.ListProjects(mapper.ctx). + Limit(strconv.FormatInt(limit, 10)). + Offset(strconv.FormatInt(offset, 10)). + Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + "Error listing projects to resolve the provided identifier to an object ID", + err, + resp, + ) + } + + projects := res.GetProjects() + if len(projects) == 0 { + break + } + + for i := range projects { + project := &projects[i] + radix.Insert(getKey(project.GetId()), project) + } + + page++ + offset = page * limit + if int64(len(projects)) < limit { + break + } + } + + minLength := radix.MinimalLength(8) + err := radix.ForEach(func(key Key, value Value) error { + project := value.(*koyeb.Project) + id := project.GetId() + name := project.GetName() + sid := getShortID(id, minLength) + + mapper.sidMap.Set(id, sid) + mapper.nameMap.Set(id, name) + mapper.names = append(mapper.names, name) + + return nil + }) + if err != nil { + return err + } + + mapper.fetched = true + + return nil +} diff --git a/pkg/koyeb/idmapper/secret.go b/pkg/koyeb/idmapper/secret.go index eb49fd2e..ab2a7ed8 100644 --- a/pkg/koyeb/idmapper/secret.go +++ b/pkg/koyeb/idmapper/secret.go @@ -11,15 +11,17 @@ import ( type SecretMapper struct { ctx context.Context client *koyeb.APIClient + project string fetched bool sidMap *IDMap nameMap *IDMap } -func NewSecretMapper(ctx context.Context, client *koyeb.APIClient) *SecretMapper { +func NewSecretMapper(ctx context.Context, client *koyeb.APIClient, project string) *SecretMapper { return &SecretMapper{ ctx: ctx, client: client, + project: project, fetched: false, sidMap: NewIDMap(), nameMap: NewIDMap(), @@ -62,10 +64,13 @@ func (mapper *SecretMapper) fetch() error { offset := int64(0) limit := int64(100) for { - res, resp, err := mapper.client.SecretsApi.ListSecrets(mapper.ctx). + req := mapper.client.SecretsApi.ListSecrets(mapper.ctx). Limit(strconv.FormatInt(limit, 10)). - Offset(strconv.FormatInt(offset, 10)). - Execute() + Offset(strconv.FormatInt(offset, 10)) + if mapper.project != "" { + req = req.ProjectId(mapper.project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error listing secrets to resolve the provided identifier to an object ID", diff --git a/pkg/koyeb/idmapper/service.go b/pkg/koyeb/idmapper/service.go index 5fb71fa3..0f9deb11 100644 --- a/pkg/koyeb/idmapper/service.go +++ b/pkg/koyeb/idmapper/service.go @@ -13,16 +13,18 @@ type ServiceMapper struct { ctx context.Context client *koyeb.APIClient appMapper *AppMapper + project string fetched bool sidMap *IDMap slugMap *IDMap } -func NewServiceMapper(ctx context.Context, client *koyeb.APIClient, appMapper *AppMapper) *ServiceMapper { +func NewServiceMapper(ctx context.Context, client *koyeb.APIClient, appMapper *AppMapper, project string) *ServiceMapper { return &ServiceMapper{ ctx: ctx, client: client, appMapper: appMapper, + project: project, fetched: false, sidMap: NewIDMap(), slugMap: NewIDMap(), @@ -84,10 +86,13 @@ func (mapper *ServiceMapper) fetch() error { limit := int64(100) for { - res, resp, err := mapper.client.ServicesApi.ListServices(mapper.ctx). + req := mapper.client.ServicesApi.ListServices(mapper.ctx). Limit(strconv.FormatInt(limit, 10)). - Offset(strconv.FormatInt(offset, 10)). - Execute() + Offset(strconv.FormatInt(offset, 10)) + if mapper.project != "" { + req = req.ProjectId(mapper.project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error listing services to resolve the provided identifier to an object ID", diff --git a/pkg/koyeb/idmapper/volume.go b/pkg/koyeb/idmapper/volume.go index 06efe5e5..da972e5f 100644 --- a/pkg/koyeb/idmapper/volume.go +++ b/pkg/koyeb/idmapper/volume.go @@ -11,15 +11,17 @@ import ( type VolumeMapper struct { ctx context.Context client *koyeb.APIClient + project string fetched bool sidMap *IDMap nameMap *IDMap } -func NewVolumeMapper(ctx context.Context, client *koyeb.APIClient) *VolumeMapper { +func NewVolumeMapper(ctx context.Context, client *koyeb.APIClient, project string) *VolumeMapper { return &VolumeMapper{ ctx: ctx, client: client, + project: project, fetched: false, sidMap: NewIDMap(), nameMap: NewIDMap(), @@ -62,10 +64,13 @@ func (mapper *VolumeMapper) fetch() error { offset := int64(0) limit := int64(100) for { - res, resp, err := mapper.client.PersistentVolumesApi.ListPersistentVolumes(mapper.ctx). + req := mapper.client.PersistentVolumesApi.ListPersistentVolumes(mapper.ctx). Limit(strconv.FormatInt(limit, 10)). - Offset(strconv.FormatInt(offset, 10)). - Execute() + Offset(strconv.FormatInt(offset, 10)) + if mapper.project != "" { + req = req.ProjectId(mapper.project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError( "Error listing volumes to resolve the provided identifier to an object ID", diff --git a/pkg/koyeb/koyeb.go b/pkg/koyeb/koyeb.go index a4726c22..c3b0afe5 100644 --- a/pkg/koyeb/koyeb.go +++ b/pkg/koyeb/koyeb.go @@ -31,6 +31,7 @@ var ( debugFull bool debug bool organization string + project string loginCmd = &cobra.Command{ Use: "login", @@ -76,7 +77,7 @@ func GetRootCommand() *cobra.Command { return err } DetectUpdates() - return SetupCLIContext(cmd, organization) + return SetupCLIContext(cmd, organization, project) }, } @@ -91,33 +92,38 @@ func GetRootCommand() *cobra.Command { 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") + rootCmd.PersistentFlags().StringVarP(&project, "project", "p", "", "project ID or name") // 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("organization", rootCmd.PersistentFlags().Lookup("organization")) //nolint:errcheck + if err := rootCmd.RegisterFlagCompletionFunc("project", CompleteProjectArgs); err != nil { + panic(err) + } rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(completionCmd) - rootCmd.AddCommand(NewOrganizationCmd()) - rootCmd.AddCommand(NewSecretCmd()) rootCmd.AddCommand(NewAppCmd()) + rootCmd.AddCommand(NewArchiveCmd()) + rootCmd.AddCommand(NewComposeCmd()) + rootCmd.AddCommand(NewDatabaseCmd()) + rootCmd.AddCommand(NewDeployCmd()) + rootCmd.AddCommand(NewDeploymentCmd()) rootCmd.AddCommand(NewDomainCmd()) - rootCmd.AddCommand(NewServiceCmd()) rootCmd.AddCommand(NewInstanceCmd()) - rootCmd.AddCommand(NewDeploymentCmd()) - rootCmd.AddCommand(NewRegionalDeploymentCmd()) - rootCmd.AddCommand(NewDatabaseCmd()) rootCmd.AddCommand(NewMetricsCmd()) - rootCmd.AddCommand(NewArchiveCmd()) - rootCmd.AddCommand(NewDeployCmd()) - rootCmd.AddCommand(NewVolumeCmd()) - rootCmd.AddCommand(NewSnapshotCmd()) - rootCmd.AddCommand(NewComposeCmd()) + rootCmd.AddCommand(NewOrganizationCmd()) + rootCmd.AddCommand(NewProjectCmd()) + rootCmd.AddCommand(NewRegionalDeploymentCmd()) rootCmd.AddCommand(NewSandboxCmd()) + rootCmd.AddCommand(NewSecretCmd()) + rootCmd.AddCommand(NewServiceCmd()) + rootCmd.AddCommand(NewSnapshotCmd()) + rootCmd.AddCommand(NewVolumeCmd()) rootCmd.AddCommand(NewWhoAmICmd()) return rootCmd } diff --git a/pkg/koyeb/project_context.go b/pkg/koyeb/project_context.go new file mode 100644 index 00000000..8f8a11a1 --- /dev/null +++ b/pkg/koyeb/project_context.go @@ -0,0 +1,247 @@ +package koyeb + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + apiv1 "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/spf13/cobra" +) + +func ResolveProjectArgs(ctx *CLIContext, val string) (string, error) { + return ctx.Mapper.Project().ResolveID(val) +} + +func CompleteProjectArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + mapper, err := newProjectCompletionMapper(cmd) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + names, err := mapper.Names() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + completions := make([]string, 0, len(names)) + for _, name := range names { + if strings.HasPrefix(name, toComplete) { + completions = append(completions, name) + } + } + + return completions, cobra.ShellCompDirectiveNoFileComp +} + +func getCurrentOrganizationID(ctx *CLIContext) (string, error) { + if ctx.Organization != "" { + return ctx.Organization, nil + } + + orgRes, resp, err := ctx.Client.ProfileApi.GetCurrentOrganization(ctx.Context).Execute() + if err == nil { + org := orgRes.GetOrganization() + return org.GetId(), nil + } + if resp != nil && resp.StatusCode != http.StatusUnauthorized { + return "", errors.NewCLIErrorFromAPIError("Unable to fetch the current organization", err, resp) + } + + appsRes, appResp, appErr := ctx.Client.AppsApi.ListApps(ctx.Context).Limit("1").Execute() + if appErr == nil { + apps := appsRes.GetApps() + if len(apps) > 0 && apps[0].GetOrganizationId() != "" { + return apps[0].GetOrganizationId(), nil + } + } else if appResp != nil && appResp.StatusCode != http.StatusUnauthorized { + return "", errors.NewCLIErrorFromAPIError("Unable to discover the current organization", appErr, appResp) + } + + projectsRes, projectResp, projectErr := ctx.Client.ProjectsApi.ListProjects(ctx.Context).Limit("1").Execute() + if projectErr == nil { + projects := projectsRes.GetProjects() + if len(projects) > 0 && projects[0].GetOrganizationId() != "" { + return projects[0].GetOrganizationId(), nil + } + } else if projectResp != nil && projectResp.StatusCode != http.StatusUnauthorized { + return "", errors.NewCLIErrorFromAPIError("Unable to discover the current organization", projectErr, projectResp) + } + + return "", &errors.CLIError{ + What: "Unable to determine the current organization", + Why: "the CLI could not infer which organization is currently active", + Additional: []string{ + "Specify the organization explicitly with --organization, or switch to one with `koyeb organizations switch`.", + }, + Solution: errors.SolutionFixConfig, + } +} + +type rawOrganizationReply struct { + Organization rawOrganization `json:"organization"` +} + +type rawOrganization struct { + Id string `json:"id,omitempty"` + DefaultProjectID *string `json:"default_project_id,omitempty"` +} + +func getOrganizationDefaultProjectID(ctx *CLIContext) (string, error) { + orgID, err := getCurrentOrganizationID(ctx) + if err != nil { + return "", err + } + + reply, err := getRawOrganization(ctx, orgID) + if err != nil { + return "", err + } + + if reply.Organization.DefaultProjectID == nil { + return "", nil + } + return *reply.Organization.DefaultProjectID, nil +} + +func updateOrganizationDefaultProjectID(ctx *CLIContext, projectID *string) error { + orgID, err := getCurrentOrganizationID(ctx) + if err != nil { + return err + } + + payload, err := json.Marshal(map[string]*string{ + "default_project_id": projectID, + }) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext( + ctx.Context, + http.MethodPatch, + fmt.Sprintf("%s/v1/organizations/%s?update_mask=default_project_id", strings.TrimRight(apiurl, "/"), orgID), + bytes.NewReader(payload), + ) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+ctx.Token) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "*/*") + + resp, err := (&http.Client{Transport: &DebugTransport{http.DefaultTransport}}).Do(req) + if err != nil { + return &errors.CLIError{ + What: "Unable to update the default project of the organization", + Why: "the CLI was unable to query the Koyeb API", + Orig: err, + Solution: errors.SolutionFixConfig, + } + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return &errors.CLIError{ + What: "Unable to update the default project of the organization", + Why: fmt.Sprintf("the Koyeb API returned HTTP/%d", resp.StatusCode), + Additional: []string{ + strings.TrimSpace(string(body)), + }, + Solution: errors.SolutionTryAgainOrUpdateOrIssue, + } + } + + return nil +} + +type projectSetter interface { + SetProjectId(string) +} + +func applyProjectID(payload projectSetter, project string) { + if project != "" { + payload.SetProjectId(project) + } +} + +func newProjectCompletionMapper(cmd *cobra.Command) (*idmapper.ProjectMapper, error) { + if err := initConfig(cmd.Root()); err != nil { + return nil, err + } + + apiClient, err := getApiClient() + if err != nil { + return nil, err + } + + ctx := context.Background() + ctx = context.WithValue(ctx, apiv1.ContextAccessToken, token) + + if organization != "" { + token, err := GetOrganizationToken(apiClient.OrganizationApi, ctx, organization) + if err != nil { + return nil, err + } + ctx = context.WithValue(ctx, apiv1.ContextAccessToken, token) + } + + return idmapper.NewProjectMapper(ctx, apiClient), nil +} + +func getRawOrganization(ctx *CLIContext, orgID string) (*rawOrganizationReply, error) { + req, err := http.NewRequestWithContext( + ctx.Context, + http.MethodGet, + fmt.Sprintf("%s/v1/organizations/%s", strings.TrimRight(apiurl, "/"), orgID), + nil, + ) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+ctx.Token) + req.Header.Set("Accept", "*/*") + + resp, err := (&http.Client{Transport: &DebugTransport{http.DefaultTransport}}).Do(req) + if err != nil { + return nil, &errors.CLIError{ + What: "Unable to fetch the current organization", + Why: "the CLI was unable to query the Koyeb API", + Orig: err, + Solution: errors.SolutionFixConfig, + } + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 300 { + return nil, &errors.CLIError{ + What: "Unable to fetch the current organization", + Why: fmt.Sprintf("the Koyeb API returned HTTP/%d", resp.StatusCode), + Additional: []string{ + strings.TrimSpace(string(body)), + }, + Solution: errors.SolutionTryAgainOrUpdateOrIssue, + } + } + + var reply rawOrganizationReply + if err := json.Unmarshal(body, &reply); err != nil { + return nil, err + } + + return &reply, nil +} diff --git a/pkg/koyeb/projects.go b/pkg/koyeb/projects.go new file mode 100644 index 00000000..d18dabd1 --- /dev/null +++ b/pkg/koyeb/projects.go @@ -0,0 +1,107 @@ +package koyeb + +import ( + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/spf13/cobra" +) + +func NewProjectCmd() *cobra.Command { + h := NewProjectHandler() + + projectCmd := &cobra.Command{ + Use: "projects ACTION", + Aliases: []string{"proj", "project"}, + Short: "Projects", + } + + createProjectCmd := &cobra.Command{ + Use: "create NAME", + Short: "Create project", + Args: cobra.ExactArgs(1), + RunE: WithCLIContext(func(ctx *CLIContext, cmd *cobra.Command, args []string) error { + createProject := koyeb.NewCreateProjectWithDefaults() + createProject.SetName(args[0]) + + if cmd.Flags().Changed("description") { + createProject.SetDescription(GetStringFlags(cmd, "description")) + } + + return h.Create(ctx, cmd, args, createProject) + }), + } + createProjectCmd.Flags().String("description", "", "Project description") + projectCmd.AddCommand(createProjectCmd) + + getProjectCmd := &cobra.Command{ + Use: "get NAME_OR_ID", + Short: "Get project", + Args: cobra.ExactArgs(1), + ValidArgsFunction: CompleteProjectArgs, + RunE: WithCLIContext(h.Get), + } + projectCmd.AddCommand(getProjectCmd) + + listProjectCmd := &cobra.Command{ + Use: "list", + Short: "List projects", + RunE: WithCLIContext(h.List), + } + projectCmd.AddCommand(listProjectCmd) + + describeProjectCmd := &cobra.Command{ + Use: "describe NAME_OR_ID", + Short: "Describe project", + Args: cobra.ExactArgs(1), + ValidArgsFunction: CompleteProjectArgs, + RunE: WithCLIContext(h.Describe), + } + projectCmd.AddCommand(describeProjectCmd) + + updateProjectCmd := &cobra.Command{ + Use: "update NAME_OR_ID", + Short: "Update project", + Args: cobra.ExactArgs(1), + ValidArgsFunction: CompleteProjectArgs, + RunE: WithCLIContext(func(ctx *CLIContext, cmd *cobra.Command, args []string) error { + updateProject := koyeb.NewProjectWithDefaults() + + if cmd.Flags().Changed("name") { + updateProject.SetName(GetStringFlags(cmd, "name")) + } + if cmd.Flags().Changed("description") { + updateProject.SetDescription(GetStringFlags(cmd, "description")) + } + + return h.Update(ctx, cmd, args, updateProject) + }), + } + updateProjectCmd.Flags().StringP("name", "n", "", "Change the name of the project") + updateProjectCmd.Flags().String("description", "", "Change the project description") + projectCmd.AddCommand(updateProjectCmd) + + deleteProjectCmd := &cobra.Command{ + Use: "delete NAME_OR_ID", + Short: "Delete project", + Args: cobra.ExactArgs(1), + ValidArgsFunction: CompleteProjectArgs, + RunE: WithCLIContext(h.Delete), + } + projectCmd.AddCommand(deleteProjectCmd) + + switchProjectCmd := &cobra.Command{ + Use: "switch NAME_OR_ID", + Short: "Switch the CLI context to another project", + Args: cobra.ExactArgs(1), + ValidArgsFunction: CompleteProjectArgs, + RunE: WithCLIContext(h.Switch), + } + projectCmd.AddCommand(switchProjectCmd) + + return projectCmd +} + +func NewProjectHandler() *ProjectHandler { + return &ProjectHandler{} +} + +type ProjectHandler struct{} diff --git a/pkg/koyeb/projects_create.go b/pkg/koyeb/projects_create.go new file mode 100644 index 00000000..4fe6b98c --- /dev/null +++ b/pkg/koyeb/projects_create.go @@ -0,0 +1,24 @@ +package koyeb + +import ( + "fmt" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createProject *koyeb.CreateProject) error { + res, resp, err := ctx.Client.ProjectsApi.CreateProject(ctx.Context).Project(*createProject).Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error while creating the project `%s`", args[0]), + err, + resp, + ) + } + + full := GetBoolFlags(cmd, "full") + ctx.Renderer.Render(NewGetProjectReply(&koyeb.GetProjectReply{Project: res.Project}, full)) + return nil +} diff --git a/pkg/koyeb/projects_delete.go b/pkg/koyeb/projects_delete.go new file mode 100644 index 00000000..62f8018d --- /dev/null +++ b/pkg/koyeb/projects_delete.go @@ -0,0 +1,34 @@ +package koyeb + +import ( + "fmt" + + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) Delete(ctx *CLIContext, cmd *cobra.Command, args []string) error { + project, err := ResolveProjectArgs(ctx, args[0]) + if err != nil { + return err + } + + _, resp, err := ctx.Client.ProjectsApi.DeleteProject(ctx.Context, project).Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error while deleting the project `%s`", args[0]), + err, + resp, + ) + } + + if ctx.Project != "" && ctx.Project == project { + if err := updateOrganizationDefaultProjectID(ctx, nil); err != nil { + return err + } + } + + log.Infof("Project %s deleted.", args[0]) + return nil +} diff --git a/pkg/koyeb/projects_describe.go b/pkg/koyeb/projects_describe.go new file mode 100644 index 00000000..e4848c5a --- /dev/null +++ b/pkg/koyeb/projects_describe.go @@ -0,0 +1,65 @@ +package koyeb + +import ( + "fmt" + + "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/renderer" + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) Describe(ctx *CLIContext, cmd *cobra.Command, args []string) error { + project, err := ResolveProjectArgs(ctx, args[0]) + if err != nil { + return err + } + + res, resp, err := ctx.Client.ProjectsApi.GetProject(ctx.Context, project).Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error while retrieving the project `%s`", args[0]), + err, + resp, + ) + } + + full := GetBoolFlags(cmd, "full") + ctx.Renderer.Render(NewDescribeProjectReply(res, full)) + return nil +} + +type DescribeProjectReply struct { + value *koyeb.GetProjectReply + full bool +} + +func NewDescribeProjectReply(value *koyeb.GetProjectReply, full bool) *DescribeProjectReply { + return &DescribeProjectReply{ + value: value, + full: full, + } +} + +func (DescribeProjectReply) Title() string { + return "Project" +} + +func (r *DescribeProjectReply) MarshalBinary() ([]byte, error) { + return r.value.GetProject().MarshalJSON() +} + +func (r *DescribeProjectReply) Headers() []string { + return []string{"id", "name", "description", "created_at", "updated_at"} +} + +func (r *DescribeProjectReply) Fields() []map[string]string { + item := r.value.GetProject() + return []map[string]string{map[string]string{ + "id": renderer.FormatID(item.GetId(), r.full), + "name": item.GetName(), + "description": item.GetDescription(), + "created_at": renderer.FormatTime(item.GetCreatedAt()), + "updated_at": renderer.FormatTime(item.GetUpdatedAt()), + }} +} diff --git a/pkg/koyeb/projects_get.go b/pkg/koyeb/projects_get.go new file mode 100644 index 00000000..ad899ed4 --- /dev/null +++ b/pkg/koyeb/projects_get.go @@ -0,0 +1,68 @@ +package koyeb + +import ( + "fmt" + + "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/renderer" + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) Get(ctx *CLIContext, cmd *cobra.Command, args []string) error { + project, err := ResolveProjectArgs(ctx, args[0]) + if err != nil { + return err + } + + res, resp, err := ctx.Client.ProjectsApi.GetProject(ctx.Context, project).Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error while retrieving the project `%s`", args[0]), + err, + resp, + ) + } + + full := GetBoolFlags(cmd, "full") + ctx.Renderer.Render(NewGetProjectReply(res, full)) + return nil +} + +type GetProjectReply struct { + value *koyeb.GetProjectReply + full bool +} + +func NewGetProjectReply(value *koyeb.GetProjectReply, full bool) *GetProjectReply { + return &GetProjectReply{ + value: value, + full: full, + } +} + +func (GetProjectReply) Title() string { + return "Project" +} + +func (r *GetProjectReply) MarshalBinary() ([]byte, error) { + return r.value.GetProject().MarshalJSON() +} + +func (r *GetProjectReply) Headers() []string { + return []string{"id", "name", "description", "created_at"} +} + +func (r *GetProjectReply) Fields() []map[string]string { + item := r.value.GetProject() + return []map[string]string{projectFields(item, r.full)} +} + +func projectFields(item koyeb.Project, full bool) map[string]string { + return map[string]string{ + "id": renderer.FormatID(item.GetId(), full), + "name": item.GetName(), + "description": item.GetDescription(), + "created_at": renderer.FormatTime(item.GetCreatedAt()), + } +} diff --git a/pkg/koyeb/projects_list.go b/pkg/koyeb/projects_list.go new file mode 100644 index 00000000..15be1977 --- /dev/null +++ b/pkg/koyeb/projects_list.go @@ -0,0 +1,77 @@ +package koyeb + +import ( + "strconv" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) error { + list := []koyeb.Project{} + + page := int64(0) + offset := int64(0) + limit := int64(100) + for { + res, resp, err := ctx.Client.ProjectsApi.ListProjects(ctx.Context). + Limit(strconv.FormatInt(limit, 10)). + Offset(strconv.FormatInt(offset, 10)). + Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError("Error while listing projects", err, resp) + } + + projects := res.GetProjects() + if len(projects) == 0 { + break + } + + list = append(list, projects...) + page++ + offset = page * limit + if int64(len(projects)) < limit { + break + } + } + + full := GetBoolFlags(cmd, "full") + ctx.Renderer.Render(NewListProjectsReply(&koyeb.ListProjectsReply{Projects: list}, full)) + return nil +} + +type ListProjectsReply struct { + value *koyeb.ListProjectsReply + full bool +} + +func NewListProjectsReply(value *koyeb.ListProjectsReply, full bool) *ListProjectsReply { + return &ListProjectsReply{ + value: value, + full: full, + } +} + +func (ListProjectsReply) Title() string { + return "Projects" +} + +func (r *ListProjectsReply) MarshalBinary() ([]byte, error) { + return r.value.MarshalJSON() +} + +func (r *ListProjectsReply) Headers() []string { + return []string{"id", "name", "description", "created_at"} +} + +func (r *ListProjectsReply) Fields() []map[string]string { + items := r.value.GetProjects() + resp := make([]map[string]string, 0, len(items)) + + for _, item := range items { + resp = append(resp, projectFields(item, r.full)) + } + + return resp +} diff --git a/pkg/koyeb/projects_switch.go b/pkg/koyeb/projects_switch.go new file mode 100644 index 00000000..e8d2dfad --- /dev/null +++ b/pkg/koyeb/projects_switch.go @@ -0,0 +1,14 @@ +package koyeb + +import ( + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) Switch(ctx *CLIContext, cmd *cobra.Command, args []string) error { + project, err := ResolveProjectArgs(ctx, args[0]) + if err != nil { + return err + } + + return updateOrganizationDefaultProjectID(ctx, &project) +} diff --git a/pkg/koyeb/projects_update.go b/pkg/koyeb/projects_update.go new file mode 100644 index 00000000..2d768663 --- /dev/null +++ b/pkg/koyeb/projects_update.go @@ -0,0 +1,43 @@ +package koyeb + +import ( + "fmt" + "strings" + + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" + "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" + "github.com/spf13/cobra" +) + +func (h *ProjectHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []string, updateProject *koyeb.Project) error { + project, err := ResolveProjectArgs(ctx, args[0]) + if err != nil { + return err + } + + updateMask := []string{} + if cmd.Flags().Changed("name") { + updateMask = append(updateMask, "name") + } + if cmd.Flags().Changed("description") { + updateMask = append(updateMask, "description") + } + + req := ctx.Client.ProjectsApi.UpdateProject2(ctx.Context, project).Project(*updateProject) + if len(updateMask) > 0 { + req = req.UpdateMask(strings.Join(updateMask, ",")) + } + + res, resp, err := req.Execute() + if err != nil { + return errors.NewCLIErrorFromAPIError( + fmt.Sprintf("Error while updating the project `%s`", args[0]), + err, + resp, + ) + } + + full := GetBoolFlags(cmd, "full") + ctx.Renderer.Render(NewGetProjectReply(&koyeb.GetProjectReply{Project: res.Project}, full)) + return nil +} diff --git a/pkg/koyeb/secrets_create.go b/pkg/koyeb/secrets_create.go index 99f57206..95d2341d 100644 --- a/pkg/koyeb/secrets_create.go +++ b/pkg/koyeb/secrets_create.go @@ -9,6 +9,7 @@ import ( ) func (h *SecretHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createSecret *koyeb.CreateSecret) error { + applyProjectID(createSecret, ctx.Project) res, resp, err := ctx.Client.SecretsApi.CreateSecret(ctx.Context).Secret(*createSecret).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( diff --git a/pkg/koyeb/secrets_list.go b/pkg/koyeb/secrets_list.go index 058dbce4..86b8cd96 100644 --- a/pkg/koyeb/secrets_list.go +++ b/pkg/koyeb/secrets_list.go @@ -17,8 +17,13 @@ func (h *SecretHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) offset := int64(0) limit := int64(100) for { - res, resp, err := ctx.Client.SecretsApi.ListSecrets(ctx.Context). - Limit(strconv.FormatInt(limit, 10)).Offset(strconv.FormatInt(offset, 10)).Execute() + req := ctx.Client.SecretsApi.ListSecrets(ctx.Context). + Limit(strconv.FormatInt(limit, 10)). + Offset(strconv.FormatInt(offset, 10)) + if ctx.Project != "" { + req = req.ProjectId(ctx.Project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError("Error while listing secrets", err, resp) } diff --git a/pkg/koyeb/services_create.go b/pkg/koyeb/services_create.go index cad32c80..371f269a 100644 --- a/pkg/koyeb/services_create.go +++ b/pkg/koyeb/services_create.go @@ -35,6 +35,7 @@ func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []stri } createService.SetAppId(resApp.App.GetId()) + applyProjectID(createService, ctx.Project) res, resp, err := ctx.Client.ServicesApi.CreateService(ctx.Context).Service(*createService).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( diff --git a/pkg/koyeb/services_list.go b/pkg/koyeb/services_list.go index 0509bd2e..22de73df 100644 --- a/pkg/koyeb/services_list.go +++ b/pkg/koyeb/services_list.go @@ -32,6 +32,9 @@ func (h *ServiceHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string if name != "" { req = req.Name(name) } + if ctx.Project != "" { + req = req.ProjectId(ctx.Project) + } res, resp, err := req.Limit(strconv.FormatInt(limit, 10)).Offset(strconv.FormatInt(offset, 10)).Execute() if err != nil { errTitle := "Error while listing services" diff --git a/pkg/koyeb/snapshots_create.go b/pkg/koyeb/snapshots_create.go index 96acc84d..acb3bba8 100644 --- a/pkg/koyeb/snapshots_create.go +++ b/pkg/koyeb/snapshots_create.go @@ -9,6 +9,7 @@ import ( ) func (h *SnapshotHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createSnapshot *koyeb.CreateSnapshotRequest) error { + applyProjectID(createSnapshot, ctx.Project) res, resp, err := ctx.Client.SnapshotsApi.CreateSnapshot(ctx.Context).Body(*createSnapshot).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( diff --git a/pkg/koyeb/volumes_create.go b/pkg/koyeb/volumes_create.go index 29c0cc57..268d186e 100644 --- a/pkg/koyeb/volumes_create.go +++ b/pkg/koyeb/volumes_create.go @@ -9,6 +9,7 @@ import ( ) func (h *VolumeHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createVolume *koyeb.CreatePersistentVolumeRequest) error { + applyProjectID(createVolume, ctx.Project) res, resp, err := ctx.Client.PersistentVolumesApi.CreatePersistentVolume(ctx.Context).Body(*createVolume).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( diff --git a/pkg/koyeb/volumes_list.go b/pkg/koyeb/volumes_list.go index 84d4fc24..12e0b24e 100644 --- a/pkg/koyeb/volumes_list.go +++ b/pkg/koyeb/volumes_list.go @@ -18,8 +18,13 @@ func (h *VolumeHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) offset := int64(0) limit := int64(100) for { - res, resp, err := ctx.Client.PersistentVolumesApi.ListPersistentVolumes(ctx.Context). - Limit(strconv.FormatInt(limit, 10)).Offset(strconv.FormatInt(offset, 10)).Execute() + req := ctx.Client.PersistentVolumesApi.ListPersistentVolumes(ctx.Context). + Limit(strconv.FormatInt(limit, 10)). + Offset(strconv.FormatInt(offset, 10)) + if ctx.Project != "" { + req = req.ProjectId(ctx.Project) + } + res, resp, err := req.Execute() if err != nil { return errors.NewCLIErrorFromAPIError("Error while listing secrets", err, resp) }