Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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