Skip to content
Merged
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
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-20260220105029-a97ddcaa1e92
github.com/koyeb/koyeb-api-client-go v0.0.0-20260508095705-49c1b251a0af
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-20260220105029-a97ddcaa1e92 h1:M3sDodjP5akaa2xtQytg8XJHqIilnd34KPrkrUN9kPQ=
github.com/koyeb/koyeb-api-client-go v0.0.0-20260220105029-a97ddcaa1e92/go.mod h1:+oQfFj2WL3gi9Pb+UHbob4D7xaT52mPfKyH1UvWa4PQ=
github.com/koyeb/koyeb-api-client-go v0.0.0-20260508095705-49c1b251a0af h1:oOiuGhmDqxiwrFMu2KJty5zXHOP6D6KgvvxQKdy49gM=
github.com/koyeb/koyeb-api-client-go v0.0.0-20260508095705-49c1b251a0af/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
57 changes: 48 additions & 9 deletions pkg/koyeb/sandbox.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package koyeb

import (
"encoding/base64"
"fmt"
"strconv"
"strings"
Expand Down Expand Up @@ -286,6 +285,29 @@ type SandboxInfo struct {
Domain string
SandboxSecret string
ProxyPort string // The external proxy port (from health check)
// BaseURL, when non-empty, is the sandbox API base URL derived from
// DeploymentMetadata.Sandbox.PublicUrl (with the "/koyeb-sandbox" path
// appended). When set, it takes precedence over Domain for client
// construction.
BaseURL string
// RoutingKey, when non-empty, is the value of
// DeploymentMetadata.Sandbox.RoutingKey. It is sent as the X-Routing-Key
// header on every sandbox HTTP request, in addition to the Authorization
// Bearer token.
RoutingKey string
}

// NewClient creates a SandboxClient using the connection information in i.
// When BaseURL is set (provided by the API via DeploymentMetadata.Sandbox),
// it is used as-is and the RoutingKey is sent as the X-Routing-Key header.
// Otherwise the legacy mechanism (https://{Domain}/koyeb-sandbox) is used.
// In all cases the SandboxSecret is sent as a Bearer token in the
// Authorization header.
func (i *SandboxInfo) NewClient(opts ...SandboxClientOption) *SandboxClient {
if i.BaseURL != "" {
return newSandboxClientFromBaseURL(i.BaseURL, i.SandboxSecret, i.RoutingKey, opts...)
}
return NewSandboxClient(i.Domain, i.SandboxSecret, opts...)
}

// ValidatePort checks if a port number is valid
Expand Down Expand Up @@ -440,6 +462,10 @@ func (h *SandboxHandler) fetchSandboxInfo(ctx *CLIContext, name string) (*Sandbo
}

deployment := deploymentRes.GetDeployment()

// SANDBOX_SECRET is always required: it is sent as a Bearer token in the
// Authorization header on every sandbox HTTP request, regardless of
// whether DeploymentMetadata.Sandbox is provided by the API.
definition := deployment.GetDefinition()
envVars := definition.GetEnv()

Expand All @@ -460,19 +486,32 @@ func (h *SandboxHandler) fetchSandboxInfo(ctx *CLIContext, name string) (*Sandbo
}
}

// The API returns env var values base64-encoded
decoded, err := base64.StdEncoding.DecodeString(sandboxSecret)
if err != nil {
// If it's not valid base64, use the value as-is
decoded = []byte(sandboxSecret)
// When the API exposes DeploymentMetadata.Sandbox, prefer its values:
// PublicUrl provides the host to reach (the "/koyeb-sandbox" path is
// still appended), and RoutingKey is sent in the X-Routing-Key header in
// addition to the Bearer token above.
var (
baseURL string
routingKey string
)
if deployment.HasMetadata() {
metadata := deployment.GetMetadata()
if metadata.HasSandbox() {
sandbox := metadata.GetSandbox()
if publicURL := sandbox.GetPublicUrl(); publicURL != "" {
baseURL = strings.TrimRight(publicURL, "/") + "/koyeb-sandbox"
}
routingKey = sandbox.GetRoutingKey()
}
}
sandboxSecret = string(decoded)

return &SandboxInfo{
ServiceID: serviceID,
AppID: appID,
Domain: domain,
SandboxSecret: sandboxSecret,
BaseURL: baseURL,
RoutingKey: routingKey,
}, nil
}

Expand All @@ -497,7 +536,7 @@ func (h *SandboxHandler) GetClientWithHealthCheck(ctx *CLIContext, sandboxName s
return nil, nil, err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

// Perform health check
health, err := client.Health(ctx.Context)
Expand Down Expand Up @@ -537,7 +576,7 @@ func (h *SandboxHandler) Health(ctx *CLIContext, cmd *cobra.Command, args []stri
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

health, err := client.Health(ctx.Context)
if err != nil {
Expand Down
41 changes: 36 additions & 5 deletions pkg/koyeb/sandbox_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,15 @@ var _ SandboxClientInterface = (*SandboxClient)(nil)

// SandboxClient provides HTTP client for sandbox API
type SandboxClient struct {
baseURL string
secret string
baseURL string
// secret is sent as a Bearer token in the Authorization header. It is
// always read from the SANDBOX_SECRET environment variable on the
// sandbox.
secret string
// routingKey, when non-empty, is sent in the X-Routing-Key header in
// addition to the Authorization header. It is provided by the API via
// DeploymentMetadata.Sandbox.RoutingKey.
routingKey string
httpClient *http.Client
maxRetries int
retryDelay time.Duration
Expand Down Expand Up @@ -72,13 +79,34 @@ func WithStreamTimeout(timeout time.Duration) SandboxClientOption {
}
}

// NewSandboxClient creates a new sandbox client
// NewSandboxClient creates a new sandbox client. The base URL is derived from
// the given domain by appending the legacy "/koyeb-sandbox" path. The secret
// is sent as a Bearer token in the Authorization header. This is the fallback
// used when DeploymentMetadata.Sandbox is not provided by the API.
func NewSandboxClient(domain, secret string, opts ...SandboxClientOption) *SandboxClient {
return newSandboxClient(fmt.Sprintf("https://%s/koyeb-sandbox", domain), secret, "", opts...)
}

// newSandboxClientFromBaseURL creates a sandbox client using an explicit base
// URL provided by the API via DeploymentMetadata.Sandbox.PublicUrl. The
// secret is still sent as a Bearer token in the Authorization header; the
// routingKey, when non-empty, is sent in the X-Routing-Key header in addition
// to the Authorization header.
func newSandboxClientFromBaseURL(baseURL, secret, routingKey string, opts ...SandboxClientOption) *SandboxClient {
return newSandboxClient(baseURL, secret, routingKey, opts...)
}

func newSandboxClient(baseURL, secret, routingKey string, opts ...SandboxClientOption) *SandboxClient {
c := &SandboxClient{
baseURL: fmt.Sprintf("https://%s/koyeb-sandbox", domain),
secret: secret,
baseURL: strings.TrimRight(baseURL, "/"),
secret: secret,
routingKey: routingKey,
httpClient: &http.Client{
Timeout: 2 * time.Minute,
// Wrap with DebugTransport so requests/responses to the
// sandbox base URL are dumped when the -d/--debug flag is set,
// matching the behavior of the main API client.
Transport: &DebugTransport{http.DefaultTransport},
},
maxRetries: 3,
retryDelay: time.Second,
Expand Down Expand Up @@ -145,6 +173,9 @@ func (c *SandboxClient) doRequest(ctx context.Context, method, path string, body
}

req.Header.Set("Authorization", "Bearer "+c.secret)
if c.routingKey != "" {
req.Header.Set("X-Routing-Key", c.routingKey)
}
req.Header.Set("Content-Type", "application/json")

return c.httpClient.Do(req)
Expand Down
14 changes: 7 additions & 7 deletions pkg/koyeb/sandbox_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (h *SandboxHandler) FsRead(ctx *CLIContext, cmd *cobra.Command, args []stri
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

content, err := client.ReadFile(ctx.Context, path)
if err != nil {
Expand Down Expand Up @@ -91,7 +91,7 @@ func (h *SandboxHandler) FsWrite(ctx *CLIContext, cmd *cobra.Command, args []str
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

err = client.WriteFile(ctx.Context, path, content)
if err != nil {
Expand Down Expand Up @@ -131,7 +131,7 @@ func (h *SandboxHandler) FsLs(ctx *CLIContext, cmd *cobra.Command, args []string
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

entries, err := client.ListDir(ctx.Context, path)
if err != nil {
Expand Down Expand Up @@ -193,7 +193,7 @@ func (h *SandboxHandler) FsMkdir(ctx *CLIContext, cmd *cobra.Command, args []str
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

err = client.MakeDir(ctx.Context, path)
if err != nil {
Expand Down Expand Up @@ -240,7 +240,7 @@ func (h *SandboxHandler) FsRm(ctx *CLIContext, cmd *cobra.Command, args []string
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

if recursive {
err = client.DeleteDir(ctx.Context, path)
Expand Down Expand Up @@ -286,7 +286,7 @@ func (h *SandboxHandler) FsUpload(ctx *CLIContext, cmd *cobra.Command, args []st
if err != nil {
return err
}
client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

if fileInfo.IsDir() {
if !recursive {
Expand Down Expand Up @@ -475,7 +475,7 @@ func (h *SandboxHandler) FsDownload(ctx *CLIContext, cmd *cobra.Command, args []
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

content, err := client.ReadFile(ctx.Context, remotePath)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/koyeb/sandbox_port.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (h *SandboxHandler) ExposePort(ctx *CLIContext, cmd *cobra.Command, args []
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

result, err := client.BindPort(ctx.Context, fmt.Sprintf("%d", port))
if err != nil {
Expand Down Expand Up @@ -77,7 +77,7 @@ func (h *SandboxHandler) UnexposePort(ctx *CLIContext, cmd *cobra.Command, args
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

result, err := client.UnbindPort(ctx.Context)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions pkg/koyeb/sandbox_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (h *SandboxHandler) StartProcess(ctx *CLIContext, cmd *cobra.Command, args
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

req := &ProcessRequest{
Cmd: command,
Expand Down Expand Up @@ -84,7 +84,7 @@ func (h *SandboxHandler) ListProcesses(ctx *CLIContext, cmd *cobra.Command, args
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

processes, err := client.ListProcesses(ctx.Context)
if err != nil {
Expand Down Expand Up @@ -118,7 +118,7 @@ func (h *SandboxHandler) KillProcess(ctx *CLIContext, cmd *cobra.Command, args [
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

err = client.KillProcess(ctx.Context, processID)
if err != nil {
Expand Down Expand Up @@ -155,7 +155,7 @@ func (h *SandboxHandler) ProcessLogs(ctx *CLIContext, cmd *cobra.Command, args [
return err
}

client := NewSandboxClient(info.Domain, info.SandboxSecret)
client := info.NewClient()

if follow {
log.Info("Streaming logs (press Ctrl+C to stop)...")
Expand Down
6 changes: 2 additions & 4 deletions pkg/koyeb/sandbox_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,8 @@ func (h *SandboxHandler) Run(ctx *CLIContext, cmd *cobra.Command, args []string)
return err
}

client := NewSandboxClient(
info.Domain,
info.SandboxSecret,
WithTimeout(time.Duration(timeout)*time.Second),
client := info.NewClient(
WithTimeout(time.Duration(timeout) * time.Second),
)

req := &RunRequest{
Expand Down
Loading