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: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

OPENAPI_DIR = ./pkg/api/openapi_client

all: format build

format:
go fmt ./...

Expand Down
72 changes: 72 additions & 0 deletions cmd/list-workspaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright © 2025 Codesphere Inc. <support@codesphere.com>
*/
package cmd

import (
"errors"
"fmt"

"github.com/codesphere-cloud/cs-go/pkg/out"
"github.com/jedib0t/go-pretty/v6/table"

"github.com/spf13/cobra"
)

type ListWorkspacesCmd struct {
cmd *cobra.Command
opts ListWorkspacesOptions
}

type ListWorkspacesOptions struct {
GlobalOptions
TeamId *int
}

func addListWorkspacesCmd(p *cobra.Command, opts GlobalOptions) {
l := ListWorkspacesCmd{
cmd: &cobra.Command{
Use: "workspaces",
Short: "list workspaces",
Long: `list workspaces available in Codesphere`,
Example: `
List all workspaces:

$ cs list workspaces --team-id <team-id>
`,
},
opts: ListWorkspacesOptions{GlobalOptions: opts},
}
l.cmd.RunE = l.RunE
l.parseLogCmdFlags()
p.AddCommand(l.cmd)
}

func (l *ListWorkspacesCmd) parseLogCmdFlags() {
l.opts.TeamId = l.cmd.Flags().IntP("team-id", "t", -1, "ID of team to query")
}

func (l *ListWorkspacesCmd) RunE(_ *cobra.Command, args []string) (err error) {
if l.opts.TeamId == nil || *l.opts.TeamId < 0 {
return errors.New("team ID not set or invalid, please use --team-id to set one")
}

client, err := NewClient(l.opts.GlobalOptions)
if err != nil {
return fmt.Errorf("failed to create Codesphere client: %e", err)
}

workspaces, err := client.ListWorkspaces(*l.opts.TeamId)
if err != nil {
return fmt.Errorf("failed to list workspaces: %e", err)
}

t := out.GetTableWriter()
t.AppendHeader(table.Row{"ID", "Name", "Repository"})
for _, w := range workspaces {
t.AppendRow(table.Row{w.Id, w.Name, *w.GitUrl.Get()})
}
t.Render()

return nil
}
34 changes: 34 additions & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
Copyright © 2025 Codesphere Inc. <support@codesphere.com>
*/
package cmd

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

type ListCmd struct {
cmd *cobra.Command
}

func addListCmd(rootCmd *cobra.Command, opts GlobalOptions) {
l := ListCmd{
cmd: &cobra.Command{
Use: "list",
Short: "list resources",
Long: `list resources available in Codesphere`,
Example: `
List all workspaces:

$ cs list workspaces
`,
},
}
l.parseLogCmdFlags()
rootCmd.AddCommand(l.cmd)
addListWorkspacesCmd(l.cmd, opts)
}

func (l *ListCmd) parseLogCmdFlags() {

}
65 changes: 30 additions & 35 deletions cmd/log.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright © 2025 Alex Klein <alex@codesphere.com>
Copyright © 2025 Codesphere Inc. <support@codesphere.com>
*/
package cmd

Expand All @@ -17,21 +17,20 @@ import (
"strings"
"sync"

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

type LogCmd struct {
cmd *cobra.Command
scope LogCmdScope
opts GlobalOptions
}

type LogCmdScope struct {
workspaceId *int
server *string
step *int
replica *string
api *string
}

type LogEntry struct {
Expand All @@ -52,7 +51,7 @@ type SSE struct {
data string
}

func addLogCmd(rootCmd *cobra.Command) {
func addLogCmd(rootCmd *cobra.Command, opts GlobalOptions) {
logCmd := LogCmd{
cmd: &cobra.Command{
Use: "log",
Expand All @@ -74,6 +73,7 @@ func addLogCmd(rootCmd *cobra.Command) {
Get logs from a self-hosted Codesphere installation:
log --api https://codesphere.acme.com/api -w 637128 -s app`,
},
opts: opts,
}
logCmd.cmd.RunE = logCmd.RunE
logCmd.parseLogCmdFlags()
Expand All @@ -82,44 +82,39 @@ func addLogCmd(rootCmd *cobra.Command) {

func (logCmd *LogCmd) parseLogCmdFlags() {
logCmd.scope = LogCmdScope{
api: logCmd.cmd.Flags().String("api", "", "URL of Codesphere API (can also be CS_API)"),
workspaceId: logCmd.cmd.Flags().IntP("workspace-id", "w", 0, "ID of Codesphere workspace (can also be CS_WORKSPACE_ID)"),
server: logCmd.cmd.Flags().StringP("server", "s", "", "Name of the landscape server"),
workspaceId: logCmd.cmd.Flags().IntP("workspace-id", "w", 0, "ID of Codesphere workspace (can also be CS_WORKSPACE_ID)"),
step: logCmd.cmd.Flags().IntP("step", "n", 0, "Index of execution step (default 0)"),
replica: logCmd.cmd.Flags().StringP("replica", "r", "", "ID of server replica"),
}
}

func (logCmd *LogCmd) RunE(_ *cobra.Command, args []string) (err error) {
if *logCmd.scope.workspaceId == 0 {
*logCmd.scope.workspaceId, err = strconv.Atoi(os.Getenv("CS_WORKSPACE_ID"))
func (l *LogCmd) RunE(_ *cobra.Command, args []string) (err error) {
if *l.scope.workspaceId == 0 {
*l.scope.workspaceId, err = strconv.Atoi(os.Getenv("CS_WORKSPACE_ID"))
if err != nil {
return fmt.Errorf("failed to read env var: %e", err)
}
if *logCmd.scope.workspaceId == 0 {
if *l.scope.workspaceId == 0 {
return errors.New("workspace ID required, but not provided")
}
}

if *logCmd.scope.api == "" {
*logCmd.scope.api = cs.GetApiUrl()
}

if *logCmd.scope.replica != "" {
if *logCmd.scope.server != "codesphere-ide" {
if *l.scope.replica != "" {
if *l.scope.server != "codesphere-ide" {
slog.Warn(
"Ignoring server flag (providing replica ID is sufficient).",
"replica", *logCmd.scope.replica,
"server", *logCmd.scope.server,
"replica", *l.scope.replica,
"server", *l.scope.server,
)
}
return printLogsOfReplica("", &logCmd.scope)
return l.printLogsOfReplica("")
}
if *logCmd.scope.server != "" {
return printLogsOfServer(&logCmd.scope)
if *l.scope.server != "" {
return l.printLogsOfServer()
}

err = logCmd.printAllLogs()
err = l.printAllLogs()
if err != nil {
return fmt.Errorf("failed to print logs: %e", err)
}
Expand All @@ -130,7 +125,7 @@ func (logCmd *LogCmd) RunE(_ *cobra.Command, args []string) (err error) {
func (l *LogCmd) printAllLogs() error {
fmt.Println("Printing logs of all replicas")

replicas, err := cs.GetPipelineStatus(*l.scope.workspaceId, "run")
replicas, err := GetPipelineStatus(*l.scope.workspaceId, "run")
if err != nil {
return fmt.Errorf("failed to get pipeline status: %e", err)
}
Expand All @@ -145,7 +140,7 @@ func (l *LogCmd) printAllLogs() error {
*scope.step = s
*scope.replica = replica.Replica
prefix := fmt.Sprintf("|%-10s|%s", replica.Server, replica.Replica[len(replica.Replica)-11:])
err = printLogsOfReplica(prefix, &scope)
err = l.printLogsOfReplica(prefix)
if err != nil {
fmt.Printf("Error printling logs: %e\n", err)
}
Expand All @@ -157,24 +152,24 @@ func (l *LogCmd) printAllLogs() error {
return nil
}

func printLogsOfReplica(prefix string, scope *LogCmdScope) error {
func (l *LogCmd) printLogsOfReplica(prefix string) error {
endpoint := fmt.Sprintf(
"%s/workspaces/%d/logs/run/%d/replica/%s",
*scope.api,
*scope.workspaceId,
*scope.step,
*scope.replica,
l.opts.GetApiUrl(),
*l.scope.workspaceId,
*l.scope.step,
*l.scope.replica,
)
return printLogsOfEndpoint(prefix, endpoint)
}

func printLogsOfServer(scope *LogCmdScope) error {
func (l *LogCmd) printLogsOfServer() error {
endpoint := fmt.Sprintf(
"%s/workspaces/%d/logs/run/%d/server/%s",
*scope.api,
*scope.workspaceId,
*scope.step,
*scope.server,
l.opts.GetApiUrl(),
*l.scope.workspaceId,
*l.scope.step,
*l.scope.server,
)
return printLogsOfEndpoint("", endpoint)
}
Expand All @@ -191,7 +186,7 @@ func printLogsOfEndpoint(prefix string, endpoint string) error {

// Set the Accept header to indicate SSE
req.Header.Set("Accept", "text/event-stream")
err = cs.SetAuthoriziationHeader(req)
err = SetAuthoriziationHeader(req)
if err != nil {
return fmt.Errorf("failed to set header: %e", err)
}
Expand Down
18 changes: 17 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import (
"github.com/spf13/cobra"
)

type GlobalOptions struct {
apiUrl *string
}

func (o GlobalOptions) GetApiUrl() string {
if o.apiUrl != nil {
return *o.apiUrl
}
return GetApiUrl()
}

func Execute() {
var rootCmd = &cobra.Command{
Use: "cs",
Expand All @@ -17,7 +28,12 @@ func Execute() {
via command line.`,
}

addLogCmd(rootCmd)
opts := GlobalOptions{}

addLogCmd(rootCmd, opts)
addListCmd(rootCmd, opts)

opts.apiUrl = rootCmd.PersistentFlags().StringP("api", "a", "", "URL of Codesphere API (can also be CS_API)")

err := rootCmd.Execute()
if err != nil {
Expand Down
38 changes: 33 additions & 5 deletions pkg/cs/cs.go → cmd/util.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package cs
package cmd

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"

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

type Step struct {
Expand Down Expand Up @@ -67,12 +71,36 @@ func Get(path string) (body []byte, err error) {
return
}

func SetAuthoriziationHeader(req *http.Request) error {

func GetApiToken() (string, error) {
apiToken := os.Getenv("CS_TOKEN")
if apiToken == "" {
return errors.New("CS_TOKEN env var required, but not set")
return "", errors.New("CS_TOKEN env var required, but not set")
}
return apiToken, nil
}

func SetAuthoriziationHeader(req *http.Request) error {
token, err := GetApiToken()
if err != nil {
return fmt.Errorf("failed to get API token: %e", err)
}
req.Header.Set("Authorization", "Bearer "+apiToken)

req.Header.Set("Authorization", "Bearer "+token)
return nil
}

func NewClient(opts GlobalOptions) (*api.Client, error) {
token, err := GetApiToken()
if err != nil {
return nil, fmt.Errorf("failed to get API token: %e", err)
}
apiUrl, err := url.Parse(opts.GetApiUrl())
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %e", opts.GetApiUrl(), err)
}
client := api.NewClient(context.Background(), api.Configuration{
BaseUrl: apiUrl,
Token: token,
})
return client, nil
}
11 changes: 9 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ module github.com/codesphere-cloud/cs-go

go 1.24.2

require github.com/spf13/cobra v1.9.1
require (
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/spf13/cobra v1.9.1
gopkg.in/validator.v2 v2.0.1
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
Loading