Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ define gen-doc-in-dir
cat ./$1/koyeb_databases_*.md >> ./$1/reference.md
cat ./$1/koyeb_version.md >> ./$1/reference.md
cat ./$1/koyeb_volumes.md >> ./$1/reference.md
cat ./$1/koyeb_volumes_*.md >> ./$1/reference.md
cat ./$1/koyeb_regions.md >> ./$1/reference.md
cat ./$1/koyeb_regions_*.md >> ./$1/reference.md
cat ./$1/koyeb_regional-deployments.md >> ./$1/reference.md
cat ./$1/koyeb_regional-deployments_*.md >> ./$1/reference.md
cat ./$1/koyeb_metrics.md >> ./$1/reference.md
cat ./$1/koyeb_metrics_*.md >> ./$1/reference.md
cat ./$1/koyeb_snapshots.md >> ./$1/reference.md
cat ./$1/koyeb_snapshots_*.md >> ./$1/reference.md
cat ./$1/koyeb_compose.md >> ./$1/reference.md
cat ./$1/koyeb_compose_*.md >> ./$1/reference.md
find ./$1 -type f -not -name 'reference.md' -delete
endef

Expand Down
758 changes: 739 additions & 19 deletions docs/reference.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/gofrs/uuid v4.3.0+incompatible
github.com/gorilla/websocket v1.5.0
github.com/iancoleman/strcase v0.2.0
github.com/koyeb/koyeb-api-client-go v0.0.0-20251231135535-21b1a3f8d818
github.com/koyeb/koyeb-api-client-go v0.0.0-20260204084353-bdcef71b7bcd
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/manifoldco/promptui v0.9.0
github.com/mitchellh/go-homedir v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/koyeb/koyeb-api-client-go v0.0.0-20251231135535-21b1a3f8d818 h1:gkkhTDimP+Vn3u4x7X6MzevxmFlolw4O+GJRN9g6acc=
github.com/koyeb/koyeb-api-client-go v0.0.0-20251231135535-21b1a3f8d818/go.mod h1:+oQfFj2WL3gi9Pb+UHbob4D7xaT52mPfKyH1UvWa4PQ=
github.com/koyeb/koyeb-api-client-go v0.0.0-20260204084353-bdcef71b7bcd h1:cA9pcvsP5JEWTx7t7Rs910R/NpT1h18X3XSLCNeTywI=
github.com/koyeb/koyeb-api-client-go v0.0.0-20260204084353-bdcef71b7bcd/go.mod h1:+oQfFj2WL3gi9Pb+UHbob4D7xaT52mPfKyH1UvWa4PQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down
1 change: 1 addition & 0 deletions pkg/koyeb/koyeb.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func GetRootCommand() *cobra.Command {
rootCmd.AddCommand(NewVolumeCmd())
rootCmd.AddCommand(NewSnapshotCmd())
rootCmd.AddCommand(NewComposeCmd())
rootCmd.AddCommand(NewRegionCmd())
return rootCmd
}

Expand Down
39 changes: 39 additions & 0 deletions pkg/koyeb/regions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package koyeb

import (
"github.com/spf13/cobra"
)

func NewRegionCmd() *cobra.Command {
h := NewRegionHandler()

regionCmd := &cobra.Command{
Use: "regions ACTION",
Aliases: []string{"region", "reg"},
Short: "Regions",
}

listRegionCmd := &cobra.Command{
Use: "list",
Short: "List regions",
RunE: WithCLIContext(h.List),
}
regionCmd.AddCommand(listRegionCmd)

getRegionCmd := &cobra.Command{
Use: "get NAME",
Short: "Get region",
Args: cobra.ExactArgs(1),
RunE: WithCLIContext(h.Get),
}
regionCmd.AddCommand(getRegionCmd)

return regionCmd
}

type RegionHandler struct {
}

func NewRegionHandler() *RegionHandler {
return &RegionHandler{}
}
76 changes: 76 additions & 0 deletions pkg/koyeb/regions_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package koyeb

import (
"fmt"
"os"
"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/renderer"
"github.com/spf13/cobra"
)

func (h *RegionHandler) Get(ctx *CLIContext, cmd *cobra.Command, args []string) error {
res, resp, err := ctx.Client.CatalogRegionsApi.GetRegion(ctx.Context, args[0]).Execute()
if err != nil {
return errors.NewCLIErrorFromAPIError(
fmt.Sprintf("Error while retrieving the region `%s`", args[0]),
err,
resp,
)
}

getRegionReply := NewGetRegionReply(res)

// For JSON/YAML output, use the standard renderer which includes instances
// in the serialized output. For table output, print a custom format.
if _, ok := ctx.Renderer.(*renderer.TableRenderer); ok {
region := res.GetRegion()
fmt.Fprintf(os.Stdout, "ID: %s\n", region.GetId())
fmt.Fprintf(os.Stdout, "Name: %s\n", region.GetName())
fmt.Fprintf(os.Stdout, "Scope: %s\n", region.GetScope())
fmt.Fprintf(os.Stdout, "Volumes enabled: %s\n", strconv.FormatBool(region.GetVolumesEnabled()))
fmt.Fprintf(os.Stdout, "\nInstances:\n")
for _, inst := range region.GetInstances() {
fmt.Fprintf(os.Stdout, " - %s\n", inst)
}
} else {
ctx.Renderer.Render(getRegionReply)
}
return nil
}

type GetRegionReply struct {
value *koyeb.GetRegionReply
}

func NewGetRegionReply(value *koyeb.GetRegionReply) *GetRegionReply {
return &GetRegionReply{
value: value,
}
}

func (GetRegionReply) Title() string {
return "Region"
}

func (r *GetRegionReply) MarshalBinary() ([]byte, error) {
return r.value.GetRegion().MarshalJSON()
}

func (r *GetRegionReply) Headers() []string {
return []string{"id", "name", "scope", "volumes_enabled"}
}

func (r *GetRegionReply) Fields() []map[string]string {
item := r.value.GetRegion()
fields := map[string]string{
"id": item.GetId(),
"name": item.GetName(),
"scope": item.GetScope(),
"volumes_enabled": strconv.FormatBool(item.GetVolumesEnabled()),
}

return []map[string]string{fields}
}
85 changes: 85 additions & 0 deletions pkg/koyeb/regions_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package koyeb

import (
"strconv"
"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 *RegionHandler) List(ctx *CLIContext, cmd *cobra.Command, args []string) error {
list := []koyeb.RegionListItem{}

page := int64(0)
offset := int64(0)
limit := int64(100)
for {
res, resp, err := ctx.Client.CatalogRegionsApi.ListRegions(ctx.Context).
Limit(strconv.FormatInt(limit, 10)).
Offset(strconv.FormatInt(offset, 10)).
Execute()
if err != nil {
return errors.NewCLIErrorFromAPIError(
"Error while listing the regions",
err,
resp,
)
}
for _, region := range res.GetRegions() {
if strings.EqualFold(region.GetStatus(), "available") {
list = append(list, region)
}
}

page++
offset = page * limit
if offset >= res.GetCount() {
break
}
}

listRegionsReply := NewListRegionsReply(&koyeb.ListRegionsReply{Regions: list})
ctx.Renderer.Render(listRegionsReply)
return nil
}

type ListRegionsReply struct {
value *koyeb.ListRegionsReply
}

func NewListRegionsReply(value *koyeb.ListRegionsReply) *ListRegionsReply {
return &ListRegionsReply{
value: value,
}
}

func (ListRegionsReply) Title() string {
return "Regions"
}

func (r *ListRegionsReply) MarshalBinary() ([]byte, error) {
return r.value.MarshalJSON()
}

func (r *ListRegionsReply) Headers() []string {
return []string{"id", "name", "scope", "volumes_enabled"}
}

func (r *ListRegionsReply) Fields() []map[string]string {
items := r.value.GetRegions()
resp := make([]map[string]string, 0, len(items))

for _, item := range items {
fields := map[string]string{
"id": item.GetId(),
"name": item.GetName(),
"scope": item.GetScope(),
"volumes_enabled": strconv.FormatBool(item.GetVolumesEnabled()),
}
resp = append(resp, fields)
}

return resp
}
99 changes: 92 additions & 7 deletions pkg/koyeb/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,12 @@ func (h *ServiceHandler) addServiceDefinitionFlagsForAllSources(flags *pflag.Fla
flags.Int64("autoscaling-requests-per-second", 0, "Target requests per second to trigger a scaling event. Set to 0 to disable requests per second autoscaling.")
flags.Int64("autoscaling-concurrent-requests", 0, "Target concurrent requests to trigger a scaling event. Set to 0 to disable concurrent requests autoscaling.")
flags.Int64("autoscaling-requests-response-time", 0, "Target p95 response time to trigger a scaling event (in ms). Set to 0 to disable concurrent response time autoscaling.")
flags.Duration("light-sleep-delay", 0,
"Delay after which an idle service is put to light sleep. "+
"Use duration format (e.g., '1m', '5m', '1h'). Set to 0 to disable.")
flags.Duration("deep-sleep-delay", 0,
"Delay after which an idle service is put to deep sleep. "+
"Use duration format (e.g., '5m', '30m', '1h'). Set to 0 to disable.")
flags.Bool("privileged", false, "Whether the service container should run in privileged mode")
flags.Bool("skip-cache", false, "Whether to use the cache when building the service")

Expand Down Expand Up @@ -600,7 +606,11 @@ func (h *ServiceHandler) parseServiceDefinitionFlags(ctx *CLIContext, flags *pfl
}

isFreeUsed := isFreeInstanceUsed(definition.GetInstanceTypes())
definition.SetScalings(h.parseScalings(isFreeUsed, flags, definition.Scalings))
scalings, err := h.parseScalings(isFreeUsed, flags, definition.Scalings)
if err != nil {
return err
}
definition.SetScalings(scalings)

healthchecks, err := h.parseChecks(definition.GetType(), flags, definition.HealthChecks)
if err != nil {
Expand Down Expand Up @@ -1023,7 +1033,7 @@ func (h *ServiceHandler) parseRegions(flags *pflag.FlagSet, currentRegions []str
}

// Parse --min-scale and --max-scale
func (h *ServiceHandler) parseScalings(isFreeUsed bool, flags *pflag.FlagSet, currentScalings []koyeb.DeploymentScaling) []koyeb.DeploymentScaling {
func (h *ServiceHandler) parseScalings(isFreeUsed bool, flags *pflag.FlagSet, currentScalings []koyeb.DeploymentScaling) ([]koyeb.DeploymentScaling, error) {
var minScale, maxScale int64

if flags.Lookup("min-scale").Changed {
Expand All @@ -1045,8 +1055,10 @@ func (h *ServiceHandler) parseScalings(isFreeUsed bool, flags *pflag.FlagSet, cu
scaling := koyeb.NewDeploymentScalingWithDefaults()
scaling.SetMin(minScale)
scaling.SetMax(maxScale)
h.setScalingsTargets(flags, scaling)
return []koyeb.DeploymentScaling{*scaling}
if err := h.setScalingsTargets(flags, scaling); err != nil {
return nil, err
}
return []koyeb.DeploymentScaling{*scaling}, nil
} else {
// Otherwise, update the current scaling configuration only if one of the scale flags has been provided
for idx := range currentScalings {
Expand All @@ -1056,10 +1068,12 @@ func (h *ServiceHandler) parseScalings(isFreeUsed bool, flags *pflag.FlagSet, cu
if flags.Lookup("scale").Changed || flags.Lookup("max-scale").Changed {
currentScalings[idx].SetMax(maxScale)
}
h.setScalingsTargets(flags, &currentScalings[idx])
if err := h.setScalingsTargets(flags, &currentScalings[idx]); err != nil {
return nil, err
}
}
}
return currentScalings
return currentScalings, nil
}

// setScalingsTargets updates the scaling targets in a koyeb.DeploymentScaling object based on the specified flags.
Expand All @@ -1080,7 +1094,7 @@ func (h *ServiceHandler) parseScalings(isFreeUsed bool, flags *pflag.FlagSet, cu
// Note: there is no way to easily avoid the code duplication in this function
// because NewDeploymentScalingTarget{AverageCPU,AverageMem,RequestsPerSecond,ConcurrentRequests,...}
// do not implement a common interface.
func (h *ServiceHandler) setScalingsTargets(flags *pflag.FlagSet, scaling *koyeb.DeploymentScaling) {
func (h *ServiceHandler) setScalingsTargets(flags *pflag.FlagSet, scaling *koyeb.DeploymentScaling) error {
if scaling.Targets == nil || scaling.GetMin() == scaling.GetMax() {
scaling.Targets = []koyeb.DeploymentScalingTarget{}
}
Expand Down Expand Up @@ -1234,6 +1248,77 @@ func (h *ServiceHandler) setScalingsTargets(flags *pflag.FlagSet, scaling *koyeb
}
scaling.Targets = newTargets
}

if flags.Lookup("light-sleep-delay").Changed || flags.Lookup("deep-sleep-delay").Changed {
if scaling.GetMin() > 0 {
return &errors.CLIError{
What: "Error while configuring the service",
Why: "--light-sleep-delay and --deep-sleep-delay can only be used when min-scale is 0",
Additional: []string{
"Sleep delays are only applicable to services that can scale to zero.",
"Set --min-scale 0 to enable scale-to-zero before configuring sleep delays.",
},
Orig: nil,
Solution: "Add --min-scale 0 to your command and try again",
}
}

lightSleepDuration, _ := flags.GetDuration("light-sleep-delay")
deepSleepDuration, _ := flags.GetDuration("deep-sleep-delay")
lightSleepChanged := flags.Lookup("light-sleep-delay").Changed
deepSleepChanged := flags.Lookup("deep-sleep-delay").Changed

newTargets := []koyeb.DeploymentScalingTarget{}
found := false

for _, target := range scaling.GetTargets() {
if target.HasSleepIdleDelay() {
found = true
sid := target.GetSleepIdleDelay()
if lightSleepChanged {
if lightSleepDuration > 0 {
sid.SetLightSleepValue(int64(lightSleepDuration.Seconds()))
} else {
sid.LightSleepValue = nil
}
}
if deepSleepChanged {
if deepSleepDuration > 0 {
sid.SetDeepSleepValue(int64(deepSleepDuration.Seconds()))
} else {
sid.DeepSleepValue = nil
}
}
// Remove the target entirely if both values are unset
if sid.LightSleepValue == nil && sid.DeepSleepValue == nil {
continue
}
target.SetSleepIdleDelay(sid)
newTargets = append(newTargets, target)
} else {
newTargets = append(newTargets, target)
}
}
if !found {
sid := koyeb.NewDeploymentScalingTargetSleepIdleDelay()
hasValue := false
if lightSleepChanged && lightSleepDuration > 0 {
sid.SetLightSleepValue(int64(lightSleepDuration.Seconds()))
hasValue = true
}
if deepSleepChanged && deepSleepDuration > 0 {
sid.SetDeepSleepValue(int64(deepSleepDuration.Seconds()))
hasValue = true
}
if hasValue {
target := koyeb.NewDeploymentScalingTarget()
target.SetSleepIdleDelay(*sid)
newTargets = append(newTargets, *target)
}
}
scaling.Targets = newTargets
}
return nil
}

// Parse --git-* and --docker-* flags to set deployment.Git or deployment.Docker
Expand Down
Loading
Loading