Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d76f52c
feat: update wake up command to wait for workspace to be running
OliverTrautvetter Feb 18, 2026
d6bef28
feat: add ScaleLandscapeServices method and update wake-up command to…
OliverTrautvetter Feb 20, 2026
6f5e9c8
fix: enhance curl command to support path handling and follow redirects
OliverTrautvetter Feb 23, 2026
bcc4bdd
fix: log workspace response status in waitForWorkspaceHealthy function
OliverTrautvetter Feb 25, 2026
c7d6ed0
fix: update authorization header to use X-CS-Authorization and modify…
OliverTrautvetter Feb 25, 2026
2af5f10
Merge branch 'main' into curl_and_wake_up_fix
OliverTrautvetter Feb 26, 2026
d55e89a
Merge branch 'main' into curl_and_wake_up_fix
OliverTrautvetter Feb 27, 2026
d5db257
fix: update error message for service replica count validation
OliverTrautvetter Feb 27, 2026
41eec55
chore(docs): Auto-update docs and licenses
OliverTrautvetter Feb 27, 2026
724db19
fix: lint error
OliverTrautvetter Feb 27, 2026
450d12b
Merge branch 'curl_and_wake_up_fix' of https://github.com/codesphere-…
OliverTrautvetter Feb 27, 2026
583817c
Update cli/cmd/curl.go
OliverTrautvetter Mar 4, 2026
727774a
feat: add scale command and related functionality for workspace services
OliverTrautvetter Mar 4, 2026
254d2f1
chore(docs): Auto-update docs and licenses
OliverTrautvetter Mar 4, 2026
f0c3e46
Merge branch 'main' into curl_and_wake_up_fix
OliverTrautvetter Mar 4, 2026
7300122
chore(docs): Auto-update docs and licenses
OliverTrautvetter Mar 4, 2026
2779545
fix: enhance wake-up command with logging for already running workspa…
OliverTrautvetter Mar 9, 2026
0cb8e8f
Merge branch 'main' into curl_and_wake_up_fix
OliverTrautvetter Mar 10, 2026
193117a
chore(docs): Auto-update docs and licenses
OliverTrautvetter Mar 10, 2026
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
9 changes: 9 additions & 0 deletions api/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ func (c *Client) ScaleWorkspace(wsId int, replicas int) error {
return errors.FormatAPIError(r, err)
}

// ScaleLandscapeServices scales landscape services by name.
// The services map contains service name -> replica count.
func (c *Client) ScaleLandscapeServices(wsId int, services map[string]int) error {
req := c.api.WorkspacesAPI.WorkspacesScaleLandscapeServices(c.ctx, float32(wsId)).
RequestBody(services)
resp, err := req.Execute()
return errors.FormatAPIError(resp, err)
}

// Waits for a given workspace to be running.
//
// Returns [TimedOut] error if the workspace does not become running in time.
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Client interface {
WorkspaceStatus(workspaceId int) (*api.WorkspaceStatus, error)
WaitForWorkspaceRunning(workspace *api.Workspace, timeout time.Duration) error
ScaleWorkspace(wsId int, replicas int) error
ScaleLandscapeServices(wsId int, services map[string]int) error
SetEnvVarOnWorkspace(workspaceId int, vars map[string]string) error
ExecCommand(workspaceId int, command string, workdir string, env map[string]string) (string, string, error)
ListWorkspacePlans() ([]api.WorkspacePlan, error)
Expand Down
17 changes: 12 additions & 5 deletions cli/cmd/curl.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"log"
"os"
"os/exec"
"strings"
"time"

io_pkg "github.com/codesphere-cloud/cs-go/pkg/io"
Expand Down Expand Up @@ -53,8 +54,13 @@ func (c *CurlCmd) RunE(_ *cobra.Command, args []string) error {
return fmt.Errorf("failed to get API token: %w", err)
}

path := args[0]
curlArgs := args[1:]
path := "/"
curlArgs := args

if len(args) > 0 && strings.HasPrefix(args[0], "/") {
path = args[0]
curlArgs = args[1:]
}

return c.CurlWorkspace(client, wsId, token, path, curlArgs)
}
Expand All @@ -70,9 +76,10 @@ func AddCurlCmd(rootCmd *cobra.Command, opts *GlobalOptions) {
{Cmd: "/api/health -w 1234", Desc: "GET request to health endpoint"},
{Cmd: "/api/data -w 1234 -- -XPOST -d '{\"key\":\"value\"}'", Desc: "POST request with data"},
{Cmd: "/api/endpoint -w 1234 -- -v", Desc: "verbose output"},
{Cmd: "-w 1234 -- -v", Desc: "verbose request to workspace root"},
{Cmd: "/ -- -k", Desc: "skip TLS verification"}, {Cmd: "/ -- -I", Desc: "HEAD request using workspace from env var"},
}),
Args: cobra.MinimumNArgs(1),
Args: cobra.ArbitraryArgs,
},
Opts: CurlOptions{
GlobalOptions: opts,
Expand Down Expand Up @@ -104,8 +111,8 @@ func (c *CurlCmd) CurlWorkspace(client Client, wsId int, token string, path stri
ctx, cancel := context.WithTimeout(context.Background(), c.Opts.Timeout)
defer cancel()

// Build curl command with authentication header
cmdArgs := []string{"curl", "-H", fmt.Sprintf("x-forward-security: %s", token)}
// Build curl command with authentication header and -L to follow redirects
cmdArgs := []string{"curl", "-L", "-H", fmt.Sprintf("X-CS-Authorization: Bearer %s", token)}

cmdArgs = append(cmdArgs, curlArgs...)
cmdArgs = append(cmdArgs, url)
Expand Down
20 changes: 14 additions & 6 deletions cli/cmd/curl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,16 @@ var _ = Describe("Curl", func() {
mock.Anything,
"curl",
mock.MatchedBy(func(args []string) bool {
// Verify the args contain the expected header, flag, and URL
// Verify the args contain the expected header, flag, -L and URL
hasFollowRedirects := false
hasHeader := false
hasFlag := false
hasURL := false
for i, arg := range args {
if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("x-forward-security: %s", token) {
if arg == "-L" {
hasFollowRedirects = true
}
if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("X-CS-Authorization: Bearer %s", token) {
hasHeader = true
}
if arg == "-I" {
Expand All @@ -76,7 +80,7 @@ var _ = Describe("Curl", func() {
hasURL = true
}
}
return hasHeader && hasFlag && hasURL
return hasFollowRedirects && hasHeader && hasFlag && hasURL
}),
mock.Anything,
mock.Anything,
Expand All @@ -93,18 +97,22 @@ var _ = Describe("Curl", func() {
mock.Anything,
"curl",
mock.MatchedBy(func(args []string) bool {
// Verify the URL contains the custom path
// Verify the URL contains the custom path and -L flag
hasFollowRedirects := false
hasHeader := false
hasURL := false
for i, arg := range args {
if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("x-forward-security: %s", token) {
if arg == "-L" {
hasFollowRedirects = true
}
if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("X-CS-Authorization: Bearer %s", token) {
hasHeader = true
}
if arg == "https://42-3000.dev.5.codesphere.com/custom/path" {
hasURL = true
}
}
return hasHeader && hasURL
return hasFollowRedirects && hasHeader && hasURL
}),
mock.Anything,
mock.Anything,
Expand Down
57 changes: 57 additions & 0 deletions cli/cmd/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func GetRootCmd() *cobra.Command {
AddGoCmd(rootCmd)
AddWakeUpCmd(rootCmd, &opts)
AddCurlCmd(rootCmd, &opts)
AddScaleCmd(rootCmd, &opts)

return rootCmd
}
Expand Down
25 changes: 25 additions & 0 deletions cli/cmd/scale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

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

type ScaleCmd struct {
cmd *cobra.Command
}

func AddScaleCmd(rootCmd *cobra.Command, opts *GlobalOptions) {
scale := ScaleCmd{
cmd: &cobra.Command{
Use: "scale",
Short: "Scale Codesphere resources",
Long: `Scale Codesphere resources, like landscape services of a workspace.`,
},
}
rootCmd.AddCommand(scale.cmd)

AddScaleWorkspaceCmd(scale.cmd, opts)
}
102 changes: 102 additions & 0 deletions cli/cmd/scale_workspace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"log"
"strconv"
"strings"

"github.com/codesphere-cloud/cs-go/pkg/io"
"github.com/spf13/cobra"
)

type ScaleWorkspaceCmd struct {
cmd *cobra.Command
Opts ScaleWorkspaceOpts
}

type ScaleWorkspaceOpts struct {
*GlobalOptions
Services []string // each entry is "service=replicas"
}

func (c *ScaleWorkspaceCmd) RunE(_ *cobra.Command, args []string) error {
workspaceId, err := c.Opts.GetWorkspaceId()
if err != nil {
return fmt.Errorf("failed to get workspace ID: %w", err)
}

client, err := NewClient(*c.Opts.GlobalOptions)
if err != nil {
return fmt.Errorf("failed to create Codesphere client: %w", err)
}

return c.ScaleWorkspaceServices(client, workspaceId)
}

func AddScaleWorkspaceCmd(scale *cobra.Command, opts *GlobalOptions) {
workspace := ScaleWorkspaceCmd{
cmd: &cobra.Command{
Use: "workspace",
Short: "Scale landscape services of a workspace",
Long: io.Long(`Scale landscape services of a workspace by specifying service name and replica count.`),
Example: io.FormatExampleCommands("scale workspace", []io.Example{
{Cmd: "--service frontend=2 --service backend=3", Desc: "scale frontend to 2 and backend to 3 replicas"},
{Cmd: "-w 1234 --service web=1", Desc: "scale web service to 1 replica on workspace 1234"},
{Cmd: "--service api=0", Desc: "scale api service to 0 replicas"},
}),
},
Opts: ScaleWorkspaceOpts{GlobalOptions: opts},
}

workspace.cmd.Flags().StringArrayVar(&workspace.Opts.Services, "service", nil, "Service to scale (format: 'service=replicas'), can be specified multiple times")
_ = workspace.cmd.MarkFlagRequired("service")

workspace.cmd.RunE = workspace.RunE

scale.AddCommand(workspace.cmd)
}

func (c *ScaleWorkspaceCmd) ScaleWorkspaceServices(client Client, wsId int) error {
services, err := parseScaleServices(c.Opts.Services)
if err != nil {
return fmt.Errorf("failed to parse services: %w", err)
}

log.Printf("Scaling landscape services for workspace %d: %v\n", wsId, services)
err = client.ScaleLandscapeServices(wsId, services)
if err != nil {
return fmt.Errorf("failed to scale landscape services: %w", err)
}

log.Printf("Landscape services scaled for workspace %d\n", wsId)
return nil
}

// parseScaleServices parses a string slice like ["web=1", "api=2"] into a map[string]int
func parseScaleServices(s []string) (map[string]int, error) {
result := make(map[string]int)

for _, pair := range s {
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid format '%s', expected 'service=replicas'", pair)
}
service := strings.TrimSpace(parts[0])
if service == "" {
return nil, fmt.Errorf("empty service name in '%s'", pair)
}
replicas, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return nil, fmt.Errorf("invalid replica count '%s' for service '%s': %w", parts[1], service, err)
}
if replicas < 0 {
return nil, fmt.Errorf("replica count must be non-negative for service '%s'", service)
}
result[service] = replicas
}
return result, nil
}
Loading
Loading