Skip to content
Draft
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 .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
run: |
echo "Cleaning up any orphaned test workspaces..."
# List all workspaces and delete any with test name prefixes
./cs list workspaces -t $CS_TEAM_ID | grep -E "cli-(test|git-test|pipeline-test|log-test|sync-test|open-test|setenv-test|edge-test)-" | awk '{print $2}' | while read ws_id; do
./cs list workspaces -t $CS_TEAM_ID | grep -E "cli-(test|git-test|pipeline-test|log-test|sync-test|open-test|setenv-test|edge-test|wakeup-test|curl-test)-" | awk '{print $2}' | while read ws_id; do
if [ ! -z "$ws_id" ]; then
echo "Deleting orphaned workspace: $ws_id"
./cs delete workspace -w $ws_id --yes || true
Expand Down
6 changes: 0 additions & 6 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,6 @@ Version: v0.3.3
License: Apache-2.0
License URL: https://github.com/xanzy/ssh-agent/blob/v0.3.3/LICENSE

----------
Module: github.com/yaml/go-yaml
Version: v2.1.0
License: Apache-2.0
License URL: https://github.com/yaml/go-yaml/blob/v2.1.0/LICENSE

----------
Module: go.yaml.in/yaml/v2
Version: v2.4.3
Expand Down
8 changes: 6 additions & 2 deletions api/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ func (client *Client) WaitForWorkspaceRunning(workspace *Workspace, timeout time
status, err := client.WorkspaceStatus(workspace.Id)

if err != nil {
// TODO: log error and retry until timeout is reached.
return errors.FormatAPIError(err)
// Retry on error (e.g., 404 if workspace not yet registered) until timeout
if client.time.Now().After(maxWaitTime) {
return errors.FormatAPIError(err)
}
client.time.Sleep(delay)
continue
}
if status.IsRunning {
return nil
Expand Down
136 changes: 136 additions & 0 deletions cli/cmd/curl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"context"
"fmt"
"log"
"os"
"os/exec"
"time"

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

type CurlCmd struct {
cmd *cobra.Command
Opts GlobalOptions
Port *int
Timeout *time.Duration
Insecure bool
}

func (c *CurlCmd) RunE(_ *cobra.Command, args []string) error {
client, err := NewClient(c.Opts)
if err != nil {
return fmt.Errorf("failed to create Codesphere client: %w", err)
}

wsId, err := c.Opts.GetWorkspaceId()
if err != nil {
return fmt.Errorf("failed to get workspace ID: %w", err)
}

token, err := c.Opts.Env.GetApiToken()
if err != nil {
return fmt.Errorf("failed to get API token: %w", err)
}

if len(args) == 0 {
return fmt.Errorf("path is required (e.g., / or /api/endpoint)")
}

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

return c.CurlWorkspace(client, wsId, token, path, curlArgs)
}

func AddCurlCmd(rootCmd *cobra.Command, opts GlobalOptions) {
curl := CurlCmd{
cmd: &cobra.Command{
Use: "curl [path] [-- curl-args...]",
Short: "Send authenticated HTTP requests to workspace dev domain",
Long: `Send authenticated HTTP requests to a workspace's development domain using curl-like syntax.`,
Example: io.FormatExampleCommands("curl", []io.Example{
{Cmd: "/ -w 1234", Desc: "GET request to workspace root"},
{Cmd: "/api/health -w 1234 -p 3001", Desc: "GET request to port 3001"},
{Cmd: "/api/data -w 1234 -- -XPOST -d '{\"key\":\"value\"}'", Desc: "POST request with data"},
{Cmd: "/api/endpoint -w 1234 -- -v", Desc: "verbose output"},
{Cmd: "/ -- -I", Desc: "HEAD request using workspace from env var"},
}),
Args: cobra.MinimumNArgs(1),
},
Opts: opts,
}
curl.Port = curl.cmd.Flags().IntP("port", "p", 3000, "Port to connect to")
curl.Timeout = curl.cmd.Flags().DurationP("timeout", "", 30*time.Second, "Timeout for the request")
curl.cmd.Flags().BoolVar(&curl.Insecure, "insecure", false, "skip TLS certificate verification (for testing only)")
rootCmd.AddCommand(curl.cmd)
curl.cmd.RunE = curl.RunE
}

func (c *CurlCmd) CurlWorkspace(client Client, wsId int, token string, path string, curlArgs []string) error {
workspace, err := client.GetWorkspace(wsId)
if err != nil {
return fmt.Errorf("failed to get workspace: %w", err)
}

port := 3000
if c.Port != nil {
port = *c.Port
}

url, err := ConstructWorkspaceServiceURL(workspace, port, path)
if err != nil {
return err
}

log.Printf("Sending request to workspace %d (%s) at %s\n", wsId, workspace.Name, url)

timeout := 30 * time.Second
if c.Timeout != nil {
timeout = *c.Timeout
}

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

// Build curl command
cmdArgs := []string{"curl"}

// Add authentication header
cmdArgs = append(cmdArgs, "-H", fmt.Sprintf("x-forward-security: %s", token))

// Add insecure flag if specified
if c.Insecure {
cmdArgs = append(cmdArgs, "-k")
}

// Add user's curl arguments
cmdArgs = append(cmdArgs, curlArgs...)

// Add URL as the last argument
cmdArgs = append(cmdArgs, url)

// Execute curl command
cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("timeout exceeded while requesting workspace %d", wsId)
}
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
return fmt.Errorf("failed to execute curl: %w", err)
}

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

package cmd_test

import (
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/codesphere-cloud/cs-go/api"
"github.com/codesphere-cloud/cs-go/cli/cmd"
)

var _ = Describe("Curl", func() {
var (
mockEnv *cmd.MockEnv
mockClient *cmd.MockClient
c *cmd.CurlCmd
wsId int
teamId int
token string
port int
)

JustBeforeEach(func() {
mockClient = cmd.NewMockClient(GinkgoT())
mockEnv = cmd.NewMockEnv(GinkgoT())
wsId = 42
teamId = 21
token = "test-api-token"
port = 3000
c = &cmd.CurlCmd{
Opts: cmd.GlobalOptions{
Env: mockEnv,
WorkspaceId: &wsId,
},
Port: &port,
}
})

Context("CurlWorkspace", func() {
It("should construct the correct URL with default port", func() {
devDomain := "team-slug.codesphere.com"
workspace := api.Workspace{
Id: wsId,
TeamId: teamId,
Name: "test-workspace",
DevDomain: &devDomain,
}

mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil)

err := c.CurlWorkspace(mockClient, wsId, token, "/api/health", []string{"-I"})

Expect(err).To(HaveOccurred())
})

It("should construct the correct URL with custom port", func() {
customPort := 3001
c.Port = &customPort
devDomain := "team-slug.codesphere.com"
workspace := api.Workspace{
Id: wsId,
TeamId: teamId,
Name: "test-workspace",
DevDomain: &devDomain,
}

mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil)

err := c.CurlWorkspace(mockClient, wsId, token, "/custom/path", []string{})

Expect(err).To(HaveOccurred())
})

It("should return error if workspace has no dev domain", func() {
workspace := api.Workspace{
Id: wsId,
TeamId: teamId,
Name: "test-workspace",
DevDomain: nil,
}

mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil)

err := c.CurlWorkspace(mockClient, wsId, token, "/", []string{})

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("does not have a development domain configured"))
})

It("should return error if GetWorkspace fails", func() {
mockClient.EXPECT().GetWorkspace(wsId).Return(api.Workspace{}, fmt.Errorf("api error"))

err := c.CurlWorkspace(mockClient, wsId, token, "/", []string{})

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to get workspace"))
})
})
})
2 changes: 1 addition & 1 deletion cli/cmd/list_workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func addListWorkspacesCmd(p *cobra.Command, opts GlobalOptions) {
Short: "List workspaces",
Long: `List workspaces available in Codesphere`,
Example: io.FormatExampleCommands("list workspaces", []io.Example{
{Cmd: "--team-id <team-id>", Desc: "List all workspaces"},
{Cmd: "-t <team-id>", Desc: "List all workspaces"},
}),
},
Opts: opts,
Expand Down
2 changes: 2 additions & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func GetRootCmd() *cobra.Command {
AddSyncCmd(rootCmd, &opts)
AddUpdateCmd(rootCmd)
AddGoCmd(rootCmd)
AddWakeUpCmd(rootCmd, opts)
AddCurlCmd(rootCmd, opts)

return rootCmd
}
Expand Down
Loading
Loading