From a6587341f248641b7a01eade5e1817cff51c3305 Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Wed, 25 Mar 2026 16:51:06 +0100 Subject: [PATCH 1/2] feat(cli): Add ps and up --- .mockery.yml | 6 +- api/client.go | 48 +- api/errors/errors.go | 8 + api/mocks.go | 1121 +++++++++++++++++ api/plan.go | 6 +- api/team.go | 10 +- api/wakeup.go | 103 ++ api/wakeup_test.go | 170 +++ api/workspace.go | 30 +- api/workspace_test.go | 17 +- cli/cmd/client.go | 23 +- cli/cmd/create_workspace_test.go | 11 +- cli/cmd/curl.go | 2 +- cli/cmd/curl_test.go | 10 +- cli/cmd/delete_workspace_test.go | 10 +- cli/cmd/exec_test.go | 10 +- cli/cmd/generate_docker.go | 8 +- cli/cmd/generate_docker_test.go | 24 +- cli/cmd/generate_images.go | 6 +- cli/cmd/generate_images_test.go | 14 +- cli/cmd/generate_kubernetes.go | 6 +- cli/cmd/generate_kubernetes_test.go | 14 +- cli/cmd/list_baseimages_test.go | 11 +- cli/cmd/list_workspaces.go | 2 +- cli/cmd/list_workspaces_test.go | 16 +- cli/cmd/log.go | 12 +- cli/cmd/mocks.go | 377 +----- cli/cmd/open.go | 6 +- cli/cmd/open_workspace.go | 8 +- cli/cmd/open_workspace_test.go | 17 +- cli/cmd/ps.go | 78 ++ cli/cmd/root.go | 36 +- cli/cmd/set_env_vars_test.go | 10 +- cli/cmd/start_pipeline.go | 122 +- cli/cmd/up.go | 122 ++ cli/cmd/wakeup.go | 121 +- cli/cmd/wakeup_test.go | 178 --- int/util/workspace.go | 4 +- pkg/cs/.cs-up.yaml | 12 + pkg/cs/browser.go | 14 +- pkg/cs/env.go | 29 +- pkg/cs/mocks.go | 323 +++++ pkg/cs/state.go | 134 ++ pkg/cs/up.go | 337 +++++ pkg/cs/up_test.go | 422 +++++++ pkg/cs/util.go | 15 +- pkg/exporter/exporter.go | 6 +- pkg/exporter/exporter_test.go | 6 +- pkg/git/git.go | 118 +- pkg/git/mocks.go | 399 +++++- pkg/pipeline/pipeline.go | 147 +++ .../pipeline}/start_pipeline_test.go | 33 +- pkg/testutil/time.go | 29 + pkg/{cs => util}/file_system.go | 19 +- 54 files changed, 3865 insertions(+), 955 deletions(-) create mode 100644 api/wakeup.go create mode 100644 api/wakeup_test.go create mode 100644 cli/cmd/ps.go create mode 100644 cli/cmd/up.go delete mode 100644 cli/cmd/wakeup_test.go create mode 100644 pkg/cs/.cs-up.yaml create mode 100644 pkg/cs/mocks.go create mode 100644 pkg/cs/state.go create mode 100644 pkg/cs/up.go create mode 100644 pkg/cs/up_test.go create mode 100644 pkg/pipeline/pipeline.go rename {cli/cmd => pkg/pipeline}/start_pipeline_test.go (90%) create mode 100644 pkg/testutil/time.go rename pkg/{cs => util}/file_system.go (84%) diff --git a/.mockery.yml b/.mockery.yml index c1fe4a6..be243c2 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -9,7 +9,7 @@ formatter: gofmt log-level: info structname: "{{.Mock}}{{.InterfaceName}}" pkgname: "{{.SrcPackageName}}" -recursive: false +recursive: true require-template-schema-exists: true template: testify template-schema: "{{.Template}}.schema.json" @@ -30,6 +30,10 @@ packages: config: all: true interfaces: + github.com/codesphere-cloud/cs-go/pkg/cs: + config: + all: true + interfaces: github.com/codesphere-cloud/cs-go/pkg/io: config: all: true diff --git a/api/client.go b/api/client.go index 51d794a..9461c15 100644 --- a/api/client.go +++ b/api/client.go @@ -7,12 +7,34 @@ import ( "context" "net/http" "net/url" + "time" "github.com/codesphere-cloud/cs-go/api/errors" "github.com/codesphere-cloud/cs-go/api/openapi_client" ) -type Client struct { +type Client interface { + ListTeams() ([]Team, error) + ListWorkspaces(teamId int) ([]Workspace, error) + ListBaseimages() ([]Baseimage, error) + GetWorkspace(workspaceId int) (Workspace, error) + WorkspaceStatus(workspaceId int) (*WorkspaceStatus, error) + WaitForWorkspaceRunning(workspace *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() ([]WorkspacePlan, error) + DeployWorkspace(args DeployWorkspaceArgs) (*Workspace, error) + DeleteWorkspace(wsId int) error + StartPipelineStage(wsId int, profile string, stage string) error + GetPipelineState(wsId int, stage string) ([]PipelineStatus, error) + GitPull(wsId int, remote string, branch string) error + DeployLandscape(wsId int, profile string) error + WakeUpWorkspace(wsId int, token string, profile string, timeout time.Duration) error +} + +type RealClient struct { ctx context.Context api *openapi_client.APIClient time Time @@ -41,15 +63,15 @@ func (c Configuration) GetApiUrl() *url.URL { } // For use in tests -func NewClientWithCustomDeps(ctx context.Context, opts Configuration, api *openapi_client.APIClient, time Time) *Client { - return &Client{ +func NewClientWithCustomDeps(ctx context.Context, opts Configuration, api *openapi_client.APIClient, time Time) *RealClient { + return &RealClient{ ctx: context.WithValue(ctx, openapi_client.ContextAccessToken, opts.Token), api: api, time: time, } } -func NewClient(ctx context.Context, opts Configuration) *Client { +func NewClient(ctx context.Context, opts Configuration) *RealClient { cfg := openapi_client.NewConfiguration() cfg.HTTPClient = NewHttpClient() cfg.Servers = []openapi_client.ServerConfiguration{{ @@ -81,32 +103,32 @@ func NewHttpClient() *http.Client { } } -func (c *Client) ListDataCenters() ([]DataCenter, error) { +func (c *RealClient) ListDataCenters() ([]DataCenter, error) { datacenters, r, err := c.api.MetadataAPI.MetadataGetDatacenters(c.ctx).Execute() return datacenters, errors.FormatAPIError(r, err) } -func (c *Client) ListDomains(teamId int) ([]Domain, error) { +func (c *RealClient) ListDomains(teamId int) ([]Domain, error) { domains, r, err := c.api.DomainsAPI.DomainsListDomains(c.ctx, float32(teamId)).Execute() return domains, errors.FormatAPIError(r, err) } -func (c *Client) GetDomain(teamId int, domainName string) (*Domain, error) { +func (c *RealClient) GetDomain(teamId int, domainName string) (*Domain, error) { domain, r, err := c.api.DomainsAPI.DomainsGetDomain(c.ctx, float32(teamId), domainName).Execute() return domain, errors.FormatAPIError(r, err) } -func (c *Client) CreateDomain(teamId int, domainName string) (*Domain, error) { +func (c *RealClient) CreateDomain(teamId int, domainName string) (*Domain, error) { domain, r, err := c.api.DomainsAPI.DomainsCreateDomain(c.ctx, float32(teamId), domainName).Execute() return domain, errors.FormatAPIError(r, err) } -func (c *Client) DeleteDomain(teamId int, domainName string) error { +func (c *RealClient) DeleteDomain(teamId int, domainName string) error { r, err := c.api.DomainsAPI.DomainsDeleteDomain(c.ctx, float32(teamId), domainName).Execute() return errors.FormatAPIError(r, err) } -func (c *Client) UpdateDomain( +func (c *RealClient) UpdateDomain( teamId int, domainName string, args UpdateDomainArgs, ) (*Domain, error) { domain, r, err := c.api.DomainsAPI. @@ -116,7 +138,7 @@ func (c *Client) UpdateDomain( return domain, errors.FormatAPIError(r, err) } -func (c *Client) VerifyDomain( +func (c *RealClient) VerifyDomain( teamId int, domainName string, ) (*DomainVerificationStatus, error) { status, r, err := c.api.DomainsAPI. @@ -124,7 +146,7 @@ func (c *Client) VerifyDomain( return status, errors.FormatAPIError(r, err) } -func (c *Client) UpdateWorkspaceConnections( +func (c *RealClient) UpdateWorkspaceConnections( teamId int, domainName string, connections PathToWorkspaces, ) (*Domain, error) { req := make(map[string][]int) @@ -141,7 +163,7 @@ func (c *Client) UpdateWorkspaceConnections( return domain, errors.FormatAPIError(r, err) } -func (c *Client) ListBaseimages() ([]Baseimage, error) { +func (c *RealClient) ListBaseimages() ([]Baseimage, error) { baseimages, r, err := c.api.MetadataAPI.MetadataGetWorkspaceBaseImages(c.ctx).Execute() return baseimages, errors.FormatAPIError(r, err) } diff --git a/api/errors/errors.go b/api/errors/errors.go index f98497d..2f874d7 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -108,3 +108,11 @@ func IsRetryable(err error) bool { } return false } + +func IsNotFound(err error) bool { + if err == nil { + return false + } + msg := err.Error() + return strings.Contains(msg, "error 404") +} diff --git a/api/mocks.go b/api/mocks.go index 05a287a..2812318 100644 --- a/api/mocks.go +++ b/api/mocks.go @@ -9,6 +9,1127 @@ import ( "time" ) +// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockClient { + mock := &MockClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockClient is an autogenerated mock type for the Client type +type MockClient struct { + mock.Mock +} + +type MockClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockClient) EXPECT() *MockClient_Expecter { + return &MockClient_Expecter{mock: &_m.Mock} +} + +// DeleteWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) DeleteWorkspace(wsId int) error { + ret := _mock.Called(wsId) + + if len(ret) == 0 { + panic("no return value specified for DeleteWorkspace") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int) error); ok { + r0 = returnFunc(wsId) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_DeleteWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteWorkspace' +type MockClient_DeleteWorkspace_Call struct { + *mock.Call +} + +// DeleteWorkspace is a helper method to define mock.On call +// - wsId int +func (_e *MockClient_Expecter) DeleteWorkspace(wsId interface{}) *MockClient_DeleteWorkspace_Call { + return &MockClient_DeleteWorkspace_Call{Call: _e.mock.On("DeleteWorkspace", wsId)} +} + +func (_c *MockClient_DeleteWorkspace_Call) Run(run func(wsId int)) *MockClient_DeleteWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockClient_DeleteWorkspace_Call) Return(err error) *MockClient_DeleteWorkspace_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_DeleteWorkspace_Call) RunAndReturn(run func(wsId int) error) *MockClient_DeleteWorkspace_Call { + _c.Call.Return(run) + return _c +} + +// DeployLandscape provides a mock function for the type MockClient +func (_mock *MockClient) DeployLandscape(wsId int, profile string) error { + ret := _mock.Called(wsId, profile) + + if len(ret) == 0 { + panic("no return value specified for DeployLandscape") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, string) error); ok { + r0 = returnFunc(wsId, profile) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_DeployLandscape_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeployLandscape' +type MockClient_DeployLandscape_Call struct { + *mock.Call +} + +// DeployLandscape is a helper method to define mock.On call +// - wsId int +// - profile string +func (_e *MockClient_Expecter) DeployLandscape(wsId interface{}, profile interface{}) *MockClient_DeployLandscape_Call { + return &MockClient_DeployLandscape_Call{Call: _e.mock.On("DeployLandscape", wsId, profile)} +} + +func (_c *MockClient_DeployLandscape_Call) Run(run func(wsId int, profile string)) *MockClient_DeployLandscape_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockClient_DeployLandscape_Call) Return(err error) *MockClient_DeployLandscape_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_DeployLandscape_Call) RunAndReturn(run func(wsId int, profile string) error) *MockClient_DeployLandscape_Call { + _c.Call.Return(run) + return _c +} + +// DeployWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) DeployWorkspace(args DeployWorkspaceArgs) (*Workspace, error) { + ret := _mock.Called(args) + + if len(ret) == 0 { + panic("no return value specified for DeployWorkspace") + } + + var r0 *Workspace + var r1 error + if returnFunc, ok := ret.Get(0).(func(DeployWorkspaceArgs) (*Workspace, error)); ok { + return returnFunc(args) + } + if returnFunc, ok := ret.Get(0).(func(DeployWorkspaceArgs) *Workspace); ok { + r0 = returnFunc(args) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Workspace) + } + } + if returnFunc, ok := ret.Get(1).(func(DeployWorkspaceArgs) error); ok { + r1 = returnFunc(args) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_DeployWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeployWorkspace' +type MockClient_DeployWorkspace_Call struct { + *mock.Call +} + +// DeployWorkspace is a helper method to define mock.On call +// - args DeployWorkspaceArgs +func (_e *MockClient_Expecter) DeployWorkspace(args interface{}) *MockClient_DeployWorkspace_Call { + return &MockClient_DeployWorkspace_Call{Call: _e.mock.On("DeployWorkspace", args)} +} + +func (_c *MockClient_DeployWorkspace_Call) Run(run func(args DeployWorkspaceArgs)) *MockClient_DeployWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 DeployWorkspaceArgs + if args[0] != nil { + arg0 = args[0].(DeployWorkspaceArgs) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockClient_DeployWorkspace_Call) Return(v *Workspace, err error) *MockClient_DeployWorkspace_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *MockClient_DeployWorkspace_Call) RunAndReturn(run func(args DeployWorkspaceArgs) (*Workspace, error)) *MockClient_DeployWorkspace_Call { + _c.Call.Return(run) + return _c +} + +// ExecCommand provides a mock function for the type MockClient +func (_mock *MockClient) ExecCommand(workspaceId int, command string, workdir string, env map[string]string) (string, string, error) { + ret := _mock.Called(workspaceId, command, workdir, env) + + if len(ret) == 0 { + panic("no return value specified for ExecCommand") + } + + var r0 string + var r1 string + var r2 error + if returnFunc, ok := ret.Get(0).(func(int, string, string, map[string]string) (string, string, error)); ok { + return returnFunc(workspaceId, command, workdir, env) + } + if returnFunc, ok := ret.Get(0).(func(int, string, string, map[string]string) string); ok { + r0 = returnFunc(workspaceId, command, workdir, env) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(int, string, string, map[string]string) string); ok { + r1 = returnFunc(workspaceId, command, workdir, env) + } else { + r1 = ret.Get(1).(string) + } + if returnFunc, ok := ret.Get(2).(func(int, string, string, map[string]string) error); ok { + r2 = returnFunc(workspaceId, command, workdir, env) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockClient_ExecCommand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecCommand' +type MockClient_ExecCommand_Call struct { + *mock.Call +} + +// ExecCommand is a helper method to define mock.On call +// - workspaceId int +// - command string +// - workdir string +// - env map[string]string +func (_e *MockClient_Expecter) ExecCommand(workspaceId interface{}, command interface{}, workdir interface{}, env interface{}) *MockClient_ExecCommand_Call { + return &MockClient_ExecCommand_Call{Call: _e.mock.On("ExecCommand", workspaceId, command, workdir, env)} +} + +func (_c *MockClient_ExecCommand_Call) Run(run func(workspaceId int, command string, workdir string, env map[string]string)) *MockClient_ExecCommand_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 map[string]string + if args[3] != nil { + arg3 = args[3].(map[string]string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockClient_ExecCommand_Call) Return(s string, s1 string, err error) *MockClient_ExecCommand_Call { + _c.Call.Return(s, s1, err) + return _c +} + +func (_c *MockClient_ExecCommand_Call) RunAndReturn(run func(workspaceId int, command string, workdir string, env map[string]string) (string, string, error)) *MockClient_ExecCommand_Call { + _c.Call.Return(run) + return _c +} + +// GetPipelineState provides a mock function for the type MockClient +func (_mock *MockClient) GetPipelineState(wsId int, stage string) ([]PipelineStatus, error) { + ret := _mock.Called(wsId, stage) + + if len(ret) == 0 { + panic("no return value specified for GetPipelineState") + } + + var r0 []PipelineStatus + var r1 error + if returnFunc, ok := ret.Get(0).(func(int, string) ([]PipelineStatus, error)); ok { + return returnFunc(wsId, stage) + } + if returnFunc, ok := ret.Get(0).(func(int, string) []PipelineStatus); ok { + r0 = returnFunc(wsId, stage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]PipelineStatus) + } + } + if returnFunc, ok := ret.Get(1).(func(int, string) error); ok { + r1 = returnFunc(wsId, stage) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_GetPipelineState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPipelineState' +type MockClient_GetPipelineState_Call struct { + *mock.Call +} + +// GetPipelineState is a helper method to define mock.On call +// - wsId int +// - stage string +func (_e *MockClient_Expecter) GetPipelineState(wsId interface{}, stage interface{}) *MockClient_GetPipelineState_Call { + return &MockClient_GetPipelineState_Call{Call: _e.mock.On("GetPipelineState", wsId, stage)} +} + +func (_c *MockClient_GetPipelineState_Call) Run(run func(wsId int, stage string)) *MockClient_GetPipelineState_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockClient_GetPipelineState_Call) Return(vs []PipelineStatus, err error) *MockClient_GetPipelineState_Call { + _c.Call.Return(vs, err) + return _c +} + +func (_c *MockClient_GetPipelineState_Call) RunAndReturn(run func(wsId int, stage string) ([]PipelineStatus, error)) *MockClient_GetPipelineState_Call { + _c.Call.Return(run) + return _c +} + +// GetWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) GetWorkspace(workspaceId int) (Workspace, error) { + ret := _mock.Called(workspaceId) + + if len(ret) == 0 { + panic("no return value specified for GetWorkspace") + } + + var r0 Workspace + var r1 error + if returnFunc, ok := ret.Get(0).(func(int) (Workspace, error)); ok { + return returnFunc(workspaceId) + } + if returnFunc, ok := ret.Get(0).(func(int) Workspace); ok { + r0 = returnFunc(workspaceId) + } else { + r0 = ret.Get(0).(Workspace) + } + if returnFunc, ok := ret.Get(1).(func(int) error); ok { + r1 = returnFunc(workspaceId) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_GetWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspace' +type MockClient_GetWorkspace_Call struct { + *mock.Call +} + +// GetWorkspace is a helper method to define mock.On call +// - workspaceId int +func (_e *MockClient_Expecter) GetWorkspace(workspaceId interface{}) *MockClient_GetWorkspace_Call { + return &MockClient_GetWorkspace_Call{Call: _e.mock.On("GetWorkspace", workspaceId)} +} + +func (_c *MockClient_GetWorkspace_Call) Run(run func(workspaceId int)) *MockClient_GetWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockClient_GetWorkspace_Call) Return(v Workspace, err error) *MockClient_GetWorkspace_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *MockClient_GetWorkspace_Call) RunAndReturn(run func(workspaceId int) (Workspace, error)) *MockClient_GetWorkspace_Call { + _c.Call.Return(run) + return _c +} + +// GitPull provides a mock function for the type MockClient +func (_mock *MockClient) GitPull(wsId int, remote string, branch string) error { + ret := _mock.Called(wsId, remote, branch) + + if len(ret) == 0 { + panic("no return value specified for GitPull") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, string, string) error); ok { + r0 = returnFunc(wsId, remote, branch) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_GitPull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GitPull' +type MockClient_GitPull_Call struct { + *mock.Call +} + +// GitPull is a helper method to define mock.On call +// - wsId int +// - remote string +// - branch string +func (_e *MockClient_Expecter) GitPull(wsId interface{}, remote interface{}, branch interface{}) *MockClient_GitPull_Call { + return &MockClient_GitPull_Call{Call: _e.mock.On("GitPull", wsId, remote, branch)} +} + +func (_c *MockClient_GitPull_Call) Run(run func(wsId int, remote string, branch string)) *MockClient_GitPull_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockClient_GitPull_Call) Return(err error) *MockClient_GitPull_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_GitPull_Call) RunAndReturn(run func(wsId int, remote string, branch string) error) *MockClient_GitPull_Call { + _c.Call.Return(run) + return _c +} + +// ListBaseimages provides a mock function for the type MockClient +func (_mock *MockClient) ListBaseimages() ([]Baseimage, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for ListBaseimages") + } + + var r0 []Baseimage + var r1 error + if returnFunc, ok := ret.Get(0).(func() ([]Baseimage, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() []Baseimage); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Baseimage) + } + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_ListBaseimages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListBaseimages' +type MockClient_ListBaseimages_Call struct { + *mock.Call +} + +// ListBaseimages is a helper method to define mock.On call +func (_e *MockClient_Expecter) ListBaseimages() *MockClient_ListBaseimages_Call { + return &MockClient_ListBaseimages_Call{Call: _e.mock.On("ListBaseimages")} +} + +func (_c *MockClient_ListBaseimages_Call) Run(run func()) *MockClient_ListBaseimages_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockClient_ListBaseimages_Call) Return(vs []Baseimage, err error) *MockClient_ListBaseimages_Call { + _c.Call.Return(vs, err) + return _c +} + +func (_c *MockClient_ListBaseimages_Call) RunAndReturn(run func() ([]Baseimage, error)) *MockClient_ListBaseimages_Call { + _c.Call.Return(run) + return _c +} + +// ListTeams provides a mock function for the type MockClient +func (_mock *MockClient) ListTeams() ([]Team, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for ListTeams") + } + + var r0 []Team + var r1 error + if returnFunc, ok := ret.Get(0).(func() ([]Team, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() []Team); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Team) + } + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_ListTeams_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListTeams' +type MockClient_ListTeams_Call struct { + *mock.Call +} + +// ListTeams is a helper method to define mock.On call +func (_e *MockClient_Expecter) ListTeams() *MockClient_ListTeams_Call { + return &MockClient_ListTeams_Call{Call: _e.mock.On("ListTeams")} +} + +func (_c *MockClient_ListTeams_Call) Run(run func()) *MockClient_ListTeams_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockClient_ListTeams_Call) Return(vs []Team, err error) *MockClient_ListTeams_Call { + _c.Call.Return(vs, err) + return _c +} + +func (_c *MockClient_ListTeams_Call) RunAndReturn(run func() ([]Team, error)) *MockClient_ListTeams_Call { + _c.Call.Return(run) + return _c +} + +// ListWorkspacePlans provides a mock function for the type MockClient +func (_mock *MockClient) ListWorkspacePlans() ([]WorkspacePlan, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for ListWorkspacePlans") + } + + var r0 []WorkspacePlan + var r1 error + if returnFunc, ok := ret.Get(0).(func() ([]WorkspacePlan, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() []WorkspacePlan); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]WorkspacePlan) + } + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_ListWorkspacePlans_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListWorkspacePlans' +type MockClient_ListWorkspacePlans_Call struct { + *mock.Call +} + +// ListWorkspacePlans is a helper method to define mock.On call +func (_e *MockClient_Expecter) ListWorkspacePlans() *MockClient_ListWorkspacePlans_Call { + return &MockClient_ListWorkspacePlans_Call{Call: _e.mock.On("ListWorkspacePlans")} +} + +func (_c *MockClient_ListWorkspacePlans_Call) Run(run func()) *MockClient_ListWorkspacePlans_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockClient_ListWorkspacePlans_Call) Return(vs []WorkspacePlan, err error) *MockClient_ListWorkspacePlans_Call { + _c.Call.Return(vs, err) + return _c +} + +func (_c *MockClient_ListWorkspacePlans_Call) RunAndReturn(run func() ([]WorkspacePlan, error)) *MockClient_ListWorkspacePlans_Call { + _c.Call.Return(run) + return _c +} + +// ListWorkspaces provides a mock function for the type MockClient +func (_mock *MockClient) ListWorkspaces(teamId int) ([]Workspace, error) { + ret := _mock.Called(teamId) + + if len(ret) == 0 { + panic("no return value specified for ListWorkspaces") + } + + var r0 []Workspace + var r1 error + if returnFunc, ok := ret.Get(0).(func(int) ([]Workspace, error)); ok { + return returnFunc(teamId) + } + if returnFunc, ok := ret.Get(0).(func(int) []Workspace); ok { + r0 = returnFunc(teamId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Workspace) + } + } + if returnFunc, ok := ret.Get(1).(func(int) error); ok { + r1 = returnFunc(teamId) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_ListWorkspaces_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListWorkspaces' +type MockClient_ListWorkspaces_Call struct { + *mock.Call +} + +// ListWorkspaces is a helper method to define mock.On call +// - teamId int +func (_e *MockClient_Expecter) ListWorkspaces(teamId interface{}) *MockClient_ListWorkspaces_Call { + return &MockClient_ListWorkspaces_Call{Call: _e.mock.On("ListWorkspaces", teamId)} +} + +func (_c *MockClient_ListWorkspaces_Call) Run(run func(teamId int)) *MockClient_ListWorkspaces_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockClient_ListWorkspaces_Call) Return(vs []Workspace, err error) *MockClient_ListWorkspaces_Call { + _c.Call.Return(vs, err) + return _c +} + +func (_c *MockClient_ListWorkspaces_Call) RunAndReturn(run func(teamId int) ([]Workspace, error)) *MockClient_ListWorkspaces_Call { + _c.Call.Return(run) + return _c +} + +// ScaleLandscapeServices provides a mock function for the type MockClient +func (_mock *MockClient) ScaleLandscapeServices(wsId int, services map[string]int) error { + ret := _mock.Called(wsId, services) + + if len(ret) == 0 { + panic("no return value specified for ScaleLandscapeServices") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, map[string]int) error); ok { + r0 = returnFunc(wsId, services) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_ScaleLandscapeServices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScaleLandscapeServices' +type MockClient_ScaleLandscapeServices_Call struct { + *mock.Call +} + +// ScaleLandscapeServices is a helper method to define mock.On call +// - wsId int +// - services map[string]int +func (_e *MockClient_Expecter) ScaleLandscapeServices(wsId interface{}, services interface{}) *MockClient_ScaleLandscapeServices_Call { + return &MockClient_ScaleLandscapeServices_Call{Call: _e.mock.On("ScaleLandscapeServices", wsId, services)} +} + +func (_c *MockClient_ScaleLandscapeServices_Call) Run(run func(wsId int, services map[string]int)) *MockClient_ScaleLandscapeServices_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 map[string]int + if args[1] != nil { + arg1 = args[1].(map[string]int) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockClient_ScaleLandscapeServices_Call) Return(err error) *MockClient_ScaleLandscapeServices_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_ScaleLandscapeServices_Call) RunAndReturn(run func(wsId int, services map[string]int) error) *MockClient_ScaleLandscapeServices_Call { + _c.Call.Return(run) + return _c +} + +// ScaleWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) ScaleWorkspace(wsId int, replicas int) error { + ret := _mock.Called(wsId, replicas) + + if len(ret) == 0 { + panic("no return value specified for ScaleWorkspace") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, int) error); ok { + r0 = returnFunc(wsId, replicas) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_ScaleWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScaleWorkspace' +type MockClient_ScaleWorkspace_Call struct { + *mock.Call +} + +// ScaleWorkspace is a helper method to define mock.On call +// - wsId int +// - replicas int +func (_e *MockClient_Expecter) ScaleWorkspace(wsId interface{}, replicas interface{}) *MockClient_ScaleWorkspace_Call { + return &MockClient_ScaleWorkspace_Call{Call: _e.mock.On("ScaleWorkspace", wsId, replicas)} +} + +func (_c *MockClient_ScaleWorkspace_Call) Run(run func(wsId int, replicas int)) *MockClient_ScaleWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 int + if args[1] != nil { + arg1 = args[1].(int) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockClient_ScaleWorkspace_Call) Return(err error) *MockClient_ScaleWorkspace_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_ScaleWorkspace_Call) RunAndReturn(run func(wsId int, replicas int) error) *MockClient_ScaleWorkspace_Call { + _c.Call.Return(run) + return _c +} + +// SetEnvVarOnWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) SetEnvVarOnWorkspace(workspaceId int, vars map[string]string) error { + ret := _mock.Called(workspaceId, vars) + + if len(ret) == 0 { + panic("no return value specified for SetEnvVarOnWorkspace") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, map[string]string) error); ok { + r0 = returnFunc(workspaceId, vars) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_SetEnvVarOnWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetEnvVarOnWorkspace' +type MockClient_SetEnvVarOnWorkspace_Call struct { + *mock.Call +} + +// SetEnvVarOnWorkspace is a helper method to define mock.On call +// - workspaceId int +// - vars map[string]string +func (_e *MockClient_Expecter) SetEnvVarOnWorkspace(workspaceId interface{}, vars interface{}) *MockClient_SetEnvVarOnWorkspace_Call { + return &MockClient_SetEnvVarOnWorkspace_Call{Call: _e.mock.On("SetEnvVarOnWorkspace", workspaceId, vars)} +} + +func (_c *MockClient_SetEnvVarOnWorkspace_Call) Run(run func(workspaceId int, vars map[string]string)) *MockClient_SetEnvVarOnWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 map[string]string + if args[1] != nil { + arg1 = args[1].(map[string]string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockClient_SetEnvVarOnWorkspace_Call) Return(err error) *MockClient_SetEnvVarOnWorkspace_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_SetEnvVarOnWorkspace_Call) RunAndReturn(run func(workspaceId int, vars map[string]string) error) *MockClient_SetEnvVarOnWorkspace_Call { + _c.Call.Return(run) + return _c +} + +// StartPipelineStage provides a mock function for the type MockClient +func (_mock *MockClient) StartPipelineStage(wsId int, profile string, stage string) error { + ret := _mock.Called(wsId, profile, stage) + + if len(ret) == 0 { + panic("no return value specified for StartPipelineStage") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, string, string) error); ok { + r0 = returnFunc(wsId, profile, stage) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_StartPipelineStage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartPipelineStage' +type MockClient_StartPipelineStage_Call struct { + *mock.Call +} + +// StartPipelineStage is a helper method to define mock.On call +// - wsId int +// - profile string +// - stage string +func (_e *MockClient_Expecter) StartPipelineStage(wsId interface{}, profile interface{}, stage interface{}) *MockClient_StartPipelineStage_Call { + return &MockClient_StartPipelineStage_Call{Call: _e.mock.On("StartPipelineStage", wsId, profile, stage)} +} + +func (_c *MockClient_StartPipelineStage_Call) Run(run func(wsId int, profile string, stage string)) *MockClient_StartPipelineStage_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockClient_StartPipelineStage_Call) Return(err error) *MockClient_StartPipelineStage_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_StartPipelineStage_Call) RunAndReturn(run func(wsId int, profile string, stage string) error) *MockClient_StartPipelineStage_Call { + _c.Call.Return(run) + return _c +} + +// WaitForWorkspaceRunning provides a mock function for the type MockClient +func (_mock *MockClient) WaitForWorkspaceRunning(workspace *Workspace, timeout time.Duration) error { + ret := _mock.Called(workspace, timeout) + + if len(ret) == 0 { + panic("no return value specified for WaitForWorkspaceRunning") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(*Workspace, time.Duration) error); ok { + r0 = returnFunc(workspace, timeout) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_WaitForWorkspaceRunning_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForWorkspaceRunning' +type MockClient_WaitForWorkspaceRunning_Call struct { + *mock.Call +} + +// WaitForWorkspaceRunning is a helper method to define mock.On call +// - workspace *Workspace +// - timeout time.Duration +func (_e *MockClient_Expecter) WaitForWorkspaceRunning(workspace interface{}, timeout interface{}) *MockClient_WaitForWorkspaceRunning_Call { + return &MockClient_WaitForWorkspaceRunning_Call{Call: _e.mock.On("WaitForWorkspaceRunning", workspace, timeout)} +} + +func (_c *MockClient_WaitForWorkspaceRunning_Call) Run(run func(workspace *Workspace, timeout time.Duration)) *MockClient_WaitForWorkspaceRunning_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 *Workspace + if args[0] != nil { + arg0 = args[0].(*Workspace) + } + var arg1 time.Duration + if args[1] != nil { + arg1 = args[1].(time.Duration) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockClient_WaitForWorkspaceRunning_Call) Return(err error) *MockClient_WaitForWorkspaceRunning_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_WaitForWorkspaceRunning_Call) RunAndReturn(run func(workspace *Workspace, timeout time.Duration) error) *MockClient_WaitForWorkspaceRunning_Call { + _c.Call.Return(run) + return _c +} + +// WakeUpWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) WakeUpWorkspace(wsId int, token string, profile string, timeout time.Duration) error { + ret := _mock.Called(wsId, token, profile, timeout) + + if len(ret) == 0 { + panic("no return value specified for WakeUpWorkspace") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, string, string, time.Duration) error); ok { + r0 = returnFunc(wsId, token, profile, timeout) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_WakeUpWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WakeUpWorkspace' +type MockClient_WakeUpWorkspace_Call struct { + *mock.Call +} + +// WakeUpWorkspace is a helper method to define mock.On call +// - wsId int +// - token string +// - profile string +// - timeout time.Duration +func (_e *MockClient_Expecter) WakeUpWorkspace(wsId interface{}, token interface{}, profile interface{}, timeout interface{}) *MockClient_WakeUpWorkspace_Call { + return &MockClient_WakeUpWorkspace_Call{Call: _e.mock.On("WakeUpWorkspace", wsId, token, profile, timeout)} +} + +func (_c *MockClient_WakeUpWorkspace_Call) Run(run func(wsId int, token string, profile string, timeout time.Duration)) *MockClient_WakeUpWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockClient_WakeUpWorkspace_Call) Return(err error) *MockClient_WakeUpWorkspace_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_WakeUpWorkspace_Call) RunAndReturn(run func(wsId int, token string, profile string, timeout time.Duration) error) *MockClient_WakeUpWorkspace_Call { + _c.Call.Return(run) + return _c +} + +// WorkspaceStatus provides a mock function for the type MockClient +func (_mock *MockClient) WorkspaceStatus(workspaceId int) (*WorkspaceStatus, error) { + ret := _mock.Called(workspaceId) + + if len(ret) == 0 { + panic("no return value specified for WorkspaceStatus") + } + + var r0 *WorkspaceStatus + var r1 error + if returnFunc, ok := ret.Get(0).(func(int) (*WorkspaceStatus, error)); ok { + return returnFunc(workspaceId) + } + if returnFunc, ok := ret.Get(0).(func(int) *WorkspaceStatus); ok { + r0 = returnFunc(workspaceId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*WorkspaceStatus) + } + } + if returnFunc, ok := ret.Get(1).(func(int) error); ok { + r1 = returnFunc(workspaceId) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockClient_WorkspaceStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WorkspaceStatus' +type MockClient_WorkspaceStatus_Call struct { + *mock.Call +} + +// WorkspaceStatus is a helper method to define mock.On call +// - workspaceId int +func (_e *MockClient_Expecter) WorkspaceStatus(workspaceId interface{}) *MockClient_WorkspaceStatus_Call { + return &MockClient_WorkspaceStatus_Call{Call: _e.mock.On("WorkspaceStatus", workspaceId)} +} + +func (_c *MockClient_WorkspaceStatus_Call) Run(run func(workspaceId int)) *MockClient_WorkspaceStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockClient_WorkspaceStatus_Call) Return(v *WorkspaceStatus, err error) *MockClient_WorkspaceStatus_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *MockClient_WorkspaceStatus_Call) RunAndReturn(run func(workspaceId int) (*WorkspaceStatus, error)) *MockClient_WorkspaceStatus_Call { + _c.Call.Return(run) + return _c +} + // NewMockTime creates a new instance of MockTime. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockTime(t interface { diff --git a/api/plan.go b/api/plan.go index 6cb744b..61c5cff 100644 --- a/api/plan.go +++ b/api/plan.go @@ -12,7 +12,7 @@ import ( // Fetches the workspace plan for a given name. // // Returns [NotFound] if no plan with the given Id could be found -func (client *Client) PlanByName(name string) (WorkspacePlan, error) { +func (client *RealClient) PlanByName(name string) (WorkspacePlan, error) { plans, err := client.ListWorkspacePlans() if err != nil { return WorkspacePlan{}, err @@ -23,10 +23,10 @@ func (client *Client) PlanByName(name string) (WorkspacePlan, error) { return p, nil } } - return WorkspacePlan{}, cserrors.NotFound(fmt.Sprintf("no team with name %s found", name)) + return WorkspacePlan{}, cserrors.NotFound(fmt.Sprintf("no workspace plan with name %s found", name)) } -func (c *Client) ListWorkspacePlans() ([]WorkspacePlan, error) { +func (c *RealClient) ListWorkspacePlans() ([]WorkspacePlan, error) { plans, r, err := c.api.MetadataAPI.MetadataGetWorkspacePlans(c.ctx).Execute() return plans, cserrors.FormatAPIError(r, err) } diff --git a/api/team.go b/api/team.go index 2f259ca..0a30f8c 100644 --- a/api/team.go +++ b/api/team.go @@ -15,7 +15,7 @@ import ( // // Returns [NotFound] if no plan with the given Id could be found // Returns [Duplicated] if no plan with the given Id could be found -func (client *Client) TeamIdByName(name string) (Team, error) { +func (client *RealClient) TeamIdByName(name string) (Team, error) { teams, err := client.ListTeams() if err != nil { return Team{}, err @@ -39,17 +39,17 @@ func (client *Client) TeamIdByName(name string) (Team, error) { return matchingTeams[0], nil } -func (c *Client) ListTeams() ([]Team, error) { +func (c *RealClient) ListTeams() ([]Team, error) { teams, r, err := c.api.TeamsAPI.TeamsListTeams(c.ctx).Execute() return teams, cserrors.FormatAPIError(r, err) } -func (c *Client) GetTeam(teamId int) (*Team, error) { +func (c *RealClient) GetTeam(teamId int) (*Team, error) { team, r, err := c.api.TeamsAPI.TeamsGetTeam(c.ctx, float32(teamId)).Execute() return ConvertToTeam(team), cserrors.FormatAPIError(r, err) } -func (c *Client) CreateTeam(name string, dc int) (*Team, error) { +func (c *RealClient) CreateTeam(name string, dc int) (*Team, error) { team, r, err := c.api.TeamsAPI.TeamsCreateTeam(c.ctx). TeamsCreateTeamRequest(openapi_client.TeamsCreateTeamRequest{ Name: name, @@ -59,7 +59,7 @@ func (c *Client) CreateTeam(name string, dc int) (*Team, error) { return ConvertToTeam(team), cserrors.FormatAPIError(r, err) } -func (c *Client) DeleteTeam(teamId int) error { +func (c *RealClient) DeleteTeam(teamId int) error { r, err := c.api.TeamsAPI.TeamsDeleteTeam(c.ctx, float32(teamId)).Execute() return cserrors.FormatAPIError(r, err) } diff --git a/api/wakeup.go b/api/wakeup.go new file mode 100644 index 0000000..1bbb436 --- /dev/null +++ b/api/wakeup.go @@ -0,0 +1,103 @@ +package api + +import ( + "fmt" + "log" + "net/http" + "time" +) + +func (client *RealClient) WakeUpWorkspace(wsId int, token string, profile string, timeout time.Duration) error { + workspace, err := client.GetWorkspace(wsId) + if err != nil { + return fmt.Errorf("failed to get workspace: %w", err) + } + + // Check if workspace is already running + status, err := client.WorkspaceStatus(wsId) + if err != nil { + return fmt.Errorf("failed to get workspace status: %w", err) + } + + if !status.IsRunning { + log.Printf("Waking up workspace %d (%s)...\n", wsId, workspace.Name) + + // Scale workspace to at least 1 replica to wake it up + // If workspace already has replicas configured (but not running), preserve that count + targetReplicas := 1 + if workspace.Replicas > 1 { + targetReplicas = workspace.Replicas + } + + err = client.ScaleWorkspace(wsId, targetReplicas) + if err != nil { + return fmt.Errorf("failed to scale workspace: %w", err) + } + + log.Printf("Waiting for workspace %d to be running...\n", wsId) + err = client.WaitForWorkspaceRunning(&workspace, timeout) + if err != nil { + return fmt.Errorf("workspace did not become running: %w", err) + } + } else { + log.Printf("Workspace %d (%s) is already running\n", wsId, workspace.Name) + } + + log.Printf("Deploying landscape for workspace %d...\n", wsId) + err = client.DeployLandscape(wsId, profile) + if err != nil { + return fmt.Errorf("failed to deploy landscape: %w", err) + } + log.Printf("Landscape deployment initiated for workspace %d\n", wsId) + + if workspace.DevDomain == nil || *workspace.DevDomain == "" { + log.Printf("Workspace %d does not have a dev domain, skipping health check\n", wsId) + return nil + } + + log.Printf("Checking health of workspace %d (%s)...\n", wsId, workspace.Name) + + err = client.waitForWorkspaceHealthy(*workspace.DevDomain, token, timeout) + if err != nil { + return fmt.Errorf("workspace did not become healthy: %w", err) + } + + log.Printf("Workspace %d is healthy and ready\n", wsId) + + return nil +} + +func (c *RealClient) waitForWorkspaceHealthy(devDomain string, token string, timeout time.Duration) error { + url := fmt.Sprintf("https://%s", devDomain) + delay := 5 * time.Second + maxWaitTime := time.Now().Add(timeout) + + httpClient := &http.Client{ + Timeout: 10 * time.Second, + } + + for { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("X-CS-Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := httpClient.Do(req) + if err == nil { + _ = resp.Body.Close() + // Any HTTP response (even 502) means the workspace proxy is reachable + // and the workspace is awake. A non-200 status just means no service + // is listening on the target port yet, which is expected for fresh workspaces. + log.Printf("Workspace %s responded with status code %d\n", devDomain, resp.StatusCode) + return nil + } + + if time.Now().After(maxWaitTime) { + return fmt.Errorf("timeout waiting for workspace to be healthy at %s", url) + } + + time.Sleep(delay) + } +} diff --git a/api/wakeup_test.go b/api/wakeup_test.go new file mode 100644 index 0000000..53a33c5 --- /dev/null +++ b/api/wakeup_test.go @@ -0,0 +1,170 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package api_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/api/openapi_client" + "github.com/codesphere-cloud/cs-go/pkg/testutil" +) + +var _ = Describe("WakeUp", func() { + var ( + wsId int + teamId int + client *api.RealClient + wsApiMock *openapi_client.MockWorkspacesAPI + ) + BeforeEach(func() { + wsId = 42 + teamId = 21 + wsApiMock = openapi_client.NewMockWorkspacesAPI(GinkgoT()) + }) + + JustBeforeEach(func() { + mockTime := testutil.MockTime() + apis := openapi_client.APIClient{ + WorkspacesAPI: wsApiMock, + } + client = api.NewClientWithCustomDeps(context.TODO(), api.Configuration{}, &apis, mockTime) + }) + + Context("WakeUpWorkspace", func() { + It("should return error if GetWorkspace fails", func() { + wsApiMock.EXPECT().WorkspacesGetWorkspace(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesGetWorkspaceRequest{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesGetWorkspaceExecute(mock.Anything).Return(nil, nil, fmt.Errorf("api error")) + + err := client.WakeUpWorkspace(wsId, "", "", 0) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get workspace")) + }) + + Context("when GetWorkspace succeeds", func() { + var workspace api.Workspace + + BeforeEach(func() { + workspace = api.Workspace{ + Id: wsId, + TeamId: teamId, + Name: "test-workspace", + } + + wsApiMock.EXPECT().WorkspacesGetWorkspace(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesGetWorkspaceRequest{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesGetWorkspaceExecute(mock.Anything).Return(&workspace, nil, nil) + }) + + Context("when workspace is not running", func() { + BeforeEach(func() { + wsApiMock.EXPECT().WorkspacesGetWorkspaceStatus(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesGetWorkspaceStatusRequest{ApiService: wsApiMock}).Once() + wsApiMock.EXPECT().WorkspacesGetWorkspaceStatusExecute(mock.Anything). + Return(&api.WorkspaceStatus{IsRunning: false}, nil, nil).Once() + }) + + Context("when scaling succeeds", func() { + BeforeEach(func() { + wsApiMock.EXPECT().WorkspacesUpdateWorkspace(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesUpdateWorkspaceRequest{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesUpdateWorkspaceExecute(mock.Anything).Return(nil, nil) + wsApiMock.EXPECT().WorkspacesGetWorkspaceStatus(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesGetWorkspaceStatusRequest{ApiService: wsApiMock}).Once() + wsApiMock.EXPECT().WorkspacesGetWorkspaceStatusExecute(mock.Anything). + Return(&api.WorkspaceStatus{IsRunning: true}, nil, nil).Once() + }) + + Context("when deploying the default landscape", func() { + BeforeEach(func() { + wsApiMock.EXPECT().WorkspacesDeployLandscape(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesDeployLandscapeRequest{ApiService: wsApiMock}) + }) + + It("should wake up the workspace by scaling to 1 replica", func() { + wsApiMock.EXPECT().WorkspacesDeployLandscapeExecute(mock.Anything).Return(nil, nil) + + err := client.WakeUpWorkspace(wsId, "", "", 0) + + Expect(err).ToNot(HaveOccurred()) + }) + + It("should sync landscape when SyncLandscape flag is set", func() { + wsApiMock.EXPECT().WorkspacesDeployLandscapeExecute(mock.Anything).Return(nil, nil) + + err := client.WakeUpWorkspace(wsId, "", "", 10) + + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return error if DeployLandscape fails", func() { + wsApiMock.EXPECT().WorkspacesDeployLandscapeExecute(mock.Anything).Return(nil, fmt.Errorf("deploy error")) + + err := client.WakeUpWorkspace(wsId, "", "", 10) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to deploy landscape")) + }) + }) + + Context("when deploying a custom profile", func() { + BeforeEach(func() { + wsApiMock.EXPECT().WorkspacesDeployLandscape1(mock.Anything, float32(wsId), "prod"). + Return(openapi_client.ApiWorkspacesDeployLandscape1Request{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesDeployLandscape1Execute(mock.Anything).Return(nil, nil) + }) + + It("should sync landscape with custom profile", func() { + err := client.WakeUpWorkspace(wsId, "", "prod", 10) + + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + + It("should return error if ScaleWorkspace fails", func() { + wsApiMock.EXPECT().WorkspacesUpdateWorkspace(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesUpdateWorkspaceRequest{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesUpdateWorkspaceExecute(mock.Anything).Return(nil, fmt.Errorf("scale error")) + + err := client.WakeUpWorkspace(wsId, "", "", 0) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to scale workspace")) + }) + }) + + Context("when workspace is already running", func() { + BeforeEach(func() { + wsApiMock.EXPECT().WorkspacesGetWorkspaceStatus(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesGetWorkspaceStatusRequest{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesGetWorkspaceStatusExecute(mock.Anything). + Return(&api.WorkspaceStatus{IsRunning: true}, nil, nil) + wsApiMock.EXPECT().WorkspacesDeployLandscape(mock.Anything, float32(wsId)). + Return(openapi_client.ApiWorkspacesDeployLandscapeRequest{ApiService: wsApiMock}) + wsApiMock.EXPECT().WorkspacesDeployLandscapeExecute(mock.Anything).Return(nil, nil) + }) + + It("should return early if workspace is already running", func() { + err := client.WakeUpWorkspace(wsId, "", "", 0) + + Expect(err).ToNot(HaveOccurred()) + }) + + It("should sync landscape even when workspace is already running", func() { + err := client.WakeUpWorkspace(wsId, "", "", 10) + + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + }) +}) diff --git a/api/workspace.go b/api/workspace.go index 6cc3fef..42db443 100644 --- a/api/workspace.go +++ b/api/workspace.go @@ -12,12 +12,12 @@ import ( "time" ) -func (c *Client) ListWorkspaces(teamId int) ([]Workspace, error) { +func (c *RealClient) ListWorkspaces(teamId int) ([]Workspace, error) { workspaces, r, err := c.api.WorkspacesAPI.WorkspacesListWorkspaces(c.ctx, float32(teamId)).Execute() return workspaces, errors.FormatAPIError(r, err) } -func (c *Client) GetWorkspace(workspaceId int) (Workspace, error) { +func (c *RealClient) GetWorkspace(workspaceId int) (Workspace, error) { workspace, r, err := c.api.WorkspacesAPI.WorkspacesGetWorkspace(c.ctx, float32(workspaceId)).Execute() if workspace != nil { @@ -26,22 +26,22 @@ func (c *Client) GetWorkspace(workspaceId int) (Workspace, error) { return Workspace{}, errors.FormatAPIError(r, err) } -func (c *Client) DeleteWorkspace(workspaceId int) error { +func (c *RealClient) DeleteWorkspace(workspaceId int) error { r, err := c.api.WorkspacesAPI.WorkspacesDeleteWorkspace(c.ctx, float32(workspaceId)).Execute() return errors.FormatAPIError(r, err) } -func (c *Client) WorkspaceStatus(workspaceId int) (*WorkspaceStatus, error) { +func (c *RealClient) WorkspaceStatus(workspaceId int) (*WorkspaceStatus, error) { status, r, err := c.api.WorkspacesAPI.WorkspacesGetWorkspaceStatus(c.ctx, float32(workspaceId)).Execute() return status, errors.FormatAPIError(r, err) } -func (c *Client) CreateWorkspace(args CreateWorkspaceArgs) (*Workspace, error) { +func (c *RealClient) CreateWorkspace(args CreateWorkspaceArgs) (*Workspace, error) { workspace, r, err := c.api.WorkspacesAPI.WorkspacesCreateWorkspace(c.ctx).WorkspacesCreateWorkspaceRequest(args).Execute() return workspace, errors.FormatAPIError(r, err) } -func (c *Client) SetEnvVarOnWorkspace(workspaceId int, envVars map[string]string) error { +func (c *RealClient) SetEnvVarOnWorkspace(workspaceId int, envVars map[string]string) error { vars := []openapi_client.WorkspacesCreateWorkspaceRequestEnvInner{} for k, v := range envVars { vars = append(vars, openapi_client.WorkspacesCreateWorkspaceRequestEnvInner{ @@ -56,7 +56,7 @@ func (c *Client) SetEnvVarOnWorkspace(workspaceId int, envVars map[string]string return errors.FormatAPIError(r, err) } -func (c *Client) ExecCommand(workspaceId int, command string, workdir string, env map[string]string) (string, string, error) { +func (c *RealClient) ExecCommand(workspaceId int, command string, workdir string, env map[string]string) (string, string, error) { workdirP := &workdir if workdir == "" { @@ -80,7 +80,7 @@ func (c *Client) ExecCommand(workspaceId int, command string, workdir string, en return res.Output, res.Error, errors.FormatAPIError(r, err) } -func (c *Client) DeployLandscape(wsId int, profile string) error { +func (c *RealClient) DeployLandscape(wsId int, profile string) error { if profile == "ci.yml" || profile == "" { req := c.api.WorkspacesAPI.WorkspacesDeployLandscape(c.ctx, float32(wsId)) r, err := req.Execute() @@ -91,7 +91,7 @@ func (c *Client) DeployLandscape(wsId int, profile string) error { return errors.FormatAPIError(r, err) } -func (c *Client) StartPipelineStage(wsId int, profile string, stage string) error { +func (c *RealClient) StartPipelineStage(wsId int, profile string, stage string) error { if profile == "ci.yml" || profile == "" { req := c.api.WorkspacesAPI.WorkspacesStartPipelineStage(c.ctx, float32(wsId), stage) r, err := req.Execute() @@ -102,7 +102,7 @@ func (c *Client) StartPipelineStage(wsId int, profile string, stage string) erro return errors.FormatAPIError(r, err) } -func (c *Client) GetPipelineState(wsId int, stage string) ([]PipelineStatus, error) { +func (c *RealClient) GetPipelineState(wsId int, stage string) ([]PipelineStatus, error) { req := c.api.WorkspacesAPI.WorkspacesPipelineStatus(c.ctx, float32(wsId), stage) res, r, err := req.Execute() return res, errors.FormatAPIError(r, err) @@ -110,7 +110,7 @@ func (c *Client) GetPipelineState(wsId int, stage string) ([]PipelineStatus, err // ScaleWorkspace sets the number of replicas for a workspace. // For on-demand workspaces, setting replicas to 1 wakes up the workspace. -func (c *Client) ScaleWorkspace(wsId int, replicas int) error { +func (c *RealClient) ScaleWorkspace(wsId int, replicas int) error { req := c.api.WorkspacesAPI.WorkspacesUpdateWorkspace(c.ctx, float32(wsId)). WorkspacesUpdateWorkspaceRequest(openapi_client.WorkspacesUpdateWorkspaceRequest{ Replicas: &replicas, @@ -121,7 +121,7 @@ func (c *Client) ScaleWorkspace(wsId int, replicas int) error { // 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 { +func (c *RealClient) ScaleLandscapeServices(wsId int, services map[string]int) error { req := c.api.WorkspacesAPI.WorkspacesScaleLandscapeServices(c.ctx, float32(wsId)). RequestBody(services) resp, err := req.Execute() @@ -131,7 +131,7 @@ func (c *Client) ScaleLandscapeServices(wsId int, services map[string]int) error // Waits for a given workspace to be running. // // Returns [TimedOut] error if the workspace does not become running in time. -func (client *Client) WaitForWorkspaceRunning(workspace *Workspace, timeout time.Duration) error { +func (client *RealClient) WaitForWorkspaceRunning(workspace *Workspace, timeout time.Duration) error { delay := 5 * time.Second maxWaitTime := client.time.Now().Add(timeout) @@ -178,7 +178,7 @@ type DeployWorkspaceArgs struct { // Deploys a workspace with the given configuration. // // Returns [TimedOut] error if the timeout is reached -func (client Client) DeployWorkspace(args DeployWorkspaceArgs) (*Workspace, error) { +func (client *RealClient) DeployWorkspace(args DeployWorkspaceArgs) (*Workspace, error) { createArgs := CreateWorkspaceArgs{ TeamId: args.TeamId, Name: args.Name, @@ -221,7 +221,7 @@ func (client Client) DeployWorkspace(args DeployWorkspaceArgs) (*Workspace, erro return workspace, nil } -func (c Client) GitPull(workspaceId int, remote string, branch string) error { +func (c *RealClient) GitPull(workspaceId int, remote string, branch string) error { if remote == "" { req := c.api.WorkspacesAPI.WorkspacesGitPull(c.ctx, float32(workspaceId)) r, err := req.Execute() diff --git a/api/workspace_test.go b/api/workspace_test.go index e631c5a..b0d866e 100644 --- a/api/workspace_test.go +++ b/api/workspace_test.go @@ -15,20 +15,9 @@ import ( "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/api/errors" "github.com/codesphere-cloud/cs-go/api/openapi_client" + "github.com/codesphere-cloud/cs-go/pkg/testutil" ) -func mockTime() *api.MockTime { - currentTime := time.Unix(1746190963, 0) - m := api.NewMockTime(GinkgoT()) - m.EXPECT().Now().RunAndReturn(func() time.Time { - return currentTime - }).Maybe() - m.EXPECT().Sleep(mock.Anything).Run(func(delay time.Duration) { - currentTime = currentTime.Add(delay) - }).Maybe() - return m -} - func mockWorkspaceStatus(wsApiMock *openapi_client.MockWorkspacesAPI, workspaceId int, isRunning ...bool) { wsApiMock.EXPECT().WorkspacesGetWorkspaceStatus(mock.Anything, float32(workspaceId)). Return(openapi_client.ApiWorkspacesGetWorkspaceStatusRequest{ApiService: wsApiMock}) @@ -44,12 +33,12 @@ var _ = Describe("Workspace", func() { var ( ws api.Workspace wsApiMock *openapi_client.MockWorkspacesAPI - client *api.Client + client *api.RealClient ) BeforeEach(func() { wsApiMock = openapi_client.NewMockWorkspacesAPI(GinkgoT()) - mockTime := mockTime() + mockTime := testutil.MockTime() apis := openapi_client.APIClient{ WorkspacesAPI: wsApiMock, } diff --git a/cli/cmd/client.go b/cli/cmd/client.go index c0a3440..0821c7a 100644 --- a/cli/cmd/client.go +++ b/cli/cmd/client.go @@ -10,30 +10,11 @@ import ( "fmt" "io" "net/url" - "time" "github.com/codesphere-cloud/cs-go/api" ) -type Client interface { - ListTeams() ([]api.Team, error) - ListWorkspaces(teamId int) ([]api.Workspace, error) - ListBaseimages() ([]api.Baseimage, error) - GetWorkspace(workspaceId int) (api.Workspace, error) - 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) - DeployWorkspace(args api.DeployWorkspaceArgs) (*api.Workspace, error) - DeleteWorkspace(wsId int) error - StartPipelineStage(wsId int, profile string, stage string) error - GetPipelineState(wsId int, stage string) ([]api.PipelineStatus, error) - GitPull(wsId int, remote string, branch string) error - DeployLandscape(wsId int, profile string) error -} +type Client = api.Client // CommandExecutor abstracts command execution for testing type CommandExecutor interface { @@ -41,7 +22,7 @@ type CommandExecutor interface { } func NewClient(opts GlobalOptions) (Client, error) { - token, err := opts.Env.GetApiToken() + token, err := opts.Env().GetApiToken() if err != nil { return nil, fmt.Errorf("failed to get API token: %w", err) } diff --git a/cli/cmd/create_workspace_test.go b/cli/cmd/create_workspace_test.go index df7a022..a3cbbcf 100644 --- a/cli/cmd/create_workspace_test.go +++ b/cli/cmd/create_workspace_test.go @@ -18,7 +18,7 @@ import ( var _ = Describe("CreateWorkspace", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient c *cmd.CreateWorkspaceCmd teamId int @@ -49,15 +49,14 @@ var _ = Describe("CreateWorkspace", func() { JustBeforeEach(func() { mockClient = cmd.NewMockClient(GinkgoT()) - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) wsName = "foo-workspace" teamId = 21 c = &cmd.CreateWorkspaceCmd{ Opts: cmd.CreateWorkspaceOpts{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ TeamId: teamId, - }, + }, mockEnv), Env: &env, Repo: repo, Vpn: vpn, @@ -96,7 +95,7 @@ var _ = Describe("CreateWorkspace", func() { Context("All values are set", func() { It("Creates workspace with all flags set", func() { createCmd := &cobra.Command{Use: "create"} - opts := &cmd.GlobalOptions{Env: cs.NewEnv()} + opts := cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{}, cs.NewMockEnv(GinkgoT())) cmd.AddCreateWorkspaceCmd(createCmd, opts) diff --git a/cli/cmd/curl.go b/cli/cmd/curl.go index f16fbd1..006f6e3 100644 --- a/cli/cmd/curl.go +++ b/cli/cmd/curl.go @@ -49,7 +49,7 @@ func (c *CurlCmd) RunE(_ *cobra.Command, args []string) error { return fmt.Errorf("failed to get workspace ID: %w", err) } - token, err := c.Opts.Env.GetApiToken() + token, err := c.Opts.Env().GetApiToken() if err != nil { return fmt.Errorf("failed to get API token: %w", err) } diff --git a/cli/cmd/curl_test.go b/cli/cmd/curl_test.go index efdd48b..17266df 100644 --- a/cli/cmd/curl_test.go +++ b/cli/cmd/curl_test.go @@ -13,11 +13,12 @@ import ( "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" ) var _ = Describe("Curl", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient mockExecutor *cmd.MockCommandExecutor c *cmd.CurlCmd @@ -30,7 +31,7 @@ var _ = Describe("Curl", func() { JustBeforeEach(func() { mockClient = cmd.NewMockClient(GinkgoT()) - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) mockExecutor = cmd.NewMockCommandExecutor(GinkgoT()) wsId = 42 teamId = 21 @@ -44,10 +45,9 @@ var _ = Describe("Curl", func() { } c = &cmd.CurlCmd{ Opts: cmd.CurlOptions{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), Timeout: 30 * time.Second, Executor: mockExecutor, }, diff --git a/cli/cmd/delete_workspace_test.go b/cli/cmd/delete_workspace_test.go index dc8af52..3d00f2f 100644 --- a/cli/cmd/delete_workspace_test.go +++ b/cli/cmd/delete_workspace_test.go @@ -11,11 +11,12 @@ import ( "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" ) var _ = Describe("DeleteWorkspace", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient mockPrompt *cmd.MockPrompt c *cmd.DeleteWorkspaceCmd @@ -27,14 +28,13 @@ var _ = Describe("DeleteWorkspace", func() { JustBeforeEach(func() { mockClient = cmd.NewMockClient(GinkgoT()) - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) mockPrompt = cmd.NewMockPrompt(GinkgoT()) c = &cmd.DeleteWorkspaceCmd{ Opts: cmd.DeleteWorkspaceOpts{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), Confirmed: &confirmed, }, Prompt: mockPrompt, diff --git a/cli/cmd/exec_test.go b/cli/cmd/exec_test.go index 3542632..f6cbdad 100644 --- a/cli/cmd/exec_test.go +++ b/cli/cmd/exec_test.go @@ -8,11 +8,12 @@ import ( . "github.com/onsi/gomega" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" ) var _ = Describe("Exec", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient e *cmd.ExecCmd wsId int @@ -24,7 +25,7 @@ var _ = Describe("Exec", func() { BeforeEach(func() { envVars = []string{} mockClient = cmd.NewMockClient(GinkgoT()) - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) wsId = 42 command = "ls -al" }) @@ -32,10 +33,9 @@ var _ = Describe("Exec", func() { JustBeforeEach(func() { e = &cmd.ExecCmd{ Opts: cmd.ExecOptions{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), EnvVar: &envVars, WorkDir: &workDir, }, diff --git a/cli/cmd/generate_docker.go b/cli/cmd/generate_docker.go index 9ce62bb..92d5824 100644 --- a/cli/cmd/generate_docker.go +++ b/cli/cmd/generate_docker.go @@ -8,10 +8,10 @@ import ( "fmt" "log" - "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" "github.com/codesphere-cloud/cs-go/pkg/git" "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/cs-go/pkg/util" "github.com/spf13/cobra" ) @@ -27,7 +27,7 @@ type GenerateDockerOpts struct { } func (c *GenerateDockerCmd) RunE(cc *cobra.Command, args []string) error { - fs := cs.NewOSFileSystem(c.Opts.RepoRoot) + fs := util.NewOSFileSystem(c.Opts.RepoRoot) gitSvc := git.NewGitService(fs) exporter := exporter.NewExporterService(fs, c.Opts.Output, c.Opts.BaseImage, c.Opts.Envs, c.Opts.RepoRoot, c.Opts.Force) @@ -93,7 +93,7 @@ func AddGenerateDockerCmd(generate *cobra.Command, opts *GenerateOpts) { docker.cmd.RunE = docker.RunE } -func (c *GenerateDockerCmd) GenerateDocker(fs *cs.FileSystem, exp exporter.Exporter, git git.Git, clientFactory func() (Client, error)) error { +func (c *GenerateDockerCmd) GenerateDocker(fs *util.FileSystem, exp exporter.Exporter, git git.Git, clientFactory func() (Client, error)) error { if c.Opts.BaseImage == "" { return errors.New("baseimage is required") } @@ -128,7 +128,7 @@ func (c *GenerateDockerCmd) GenerateDocker(fs *cs.FileSystem, exp exporter.Expor return nil } -func (c *GenerateDockerCmd) CloneRepository(client Client, fs *cs.FileSystem, git git.Git, clonedir string) error { +func (c *GenerateDockerCmd) CloneRepository(client Client, fs *util.FileSystem, git git.Git, clonedir string) error { log.Printf("Cloning repository into %s...\n", clonedir) wsId, err := c.Opts.GetWorkspaceId() diff --git a/cli/cmd/generate_docker_test.go b/cli/cmd/generate_docker_test.go index 25a27a7..ce5fd13 100644 --- a/cli/cmd/generate_docker_test.go +++ b/cli/cmd/generate_docker_test.go @@ -17,12 +17,13 @@ import ( "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" "github.com/codesphere-cloud/cs-go/pkg/git" + "github.com/codesphere-cloud/cs-go/pkg/util" ) var _ = Describe("GenerateDocker", func() { var ( - memoryFs *cs.FileSystem - mockEnv *cmd.MockEnv + memoryFs *util.FileSystem + mockEnv *cs.MockEnv mockExporter *exporter.MockExporter mockGit *git.MockGit mockClient *cmd.MockClient @@ -31,8 +32,8 @@ var _ = Describe("GenerateDocker", func() { ) BeforeEach(func() { - memoryFs = cs.NewMemFileSystem() - mockEnv = cmd.NewMockEnv(GinkgoT()) + memoryFs = util.NewMemFileSystem() + mockEnv = cs.NewMockEnv(GinkgoT()) mockExporter = exporter.NewMockExporter(GinkgoT()) mockGit = git.NewMockGit(GinkgoT()) mockClient = cmd.NewMockClient(GinkgoT()) @@ -43,10 +44,9 @@ var _ = Describe("GenerateDocker", func() { c = &cmd.GenerateDockerCmd{ Opts: &cmd.GenerateDockerOpts{ GenerateOpts: &cmd.GenerateOpts{ - GlobalOptions: &cmd.GlobalOptions{ + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: -1, - Env: mockEnv, - }, + }, mockEnv), Input: defaultInput, Output: defaultOutput, }, @@ -119,11 +119,10 @@ var _ = Describe("GenerateDocker", func() { cmd := &cmd.GenerateDockerCmd{ Opts: &cmd.GenerateDockerOpts{ GenerateOpts: &cmd.GenerateOpts{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: -1, // WorkspaceId is -1 (default), so it will use env var - }, + }, mockEnv), }, BaseImage: "alpine:latest", }, @@ -147,10 +146,9 @@ var _ = Describe("GenerateDocker", func() { cmd := &cmd.GenerateDockerCmd{ Opts: &cmd.GenerateDockerOpts{ GenerateOpts: &cmd.GenerateOpts{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: flagWsId, - }, + }, mockEnv), }, BaseImage: "alpine:latest", }, diff --git a/cli/cmd/generate_images.go b/cli/cmd/generate_images.go index 7c8c6e5..28426f8 100644 --- a/cli/cmd/generate_images.go +++ b/cli/cmd/generate_images.go @@ -9,9 +9,9 @@ import ( "fmt" "log" - "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/cs-go/pkg/util" "github.com/spf13/cobra" ) @@ -27,7 +27,7 @@ type GenerateImagesOpts struct { } func (c *GenerateImagesCmd) RunE(_ *cobra.Command, args []string) error { - fs := cs.NewOSFileSystem(c.Opts.RepoRoot) + fs := util.NewOSFileSystem(c.Opts.RepoRoot) exporter := exporter.NewExporterService(fs, c.Opts.Output, "", []string{}, c.Opts.RepoRoot, c.Opts.Force) if err := c.GenerateImages(fs, exporter); err != nil { @@ -67,7 +67,7 @@ func AddGenerateImagesCmd(generate *cobra.Command, opts *GenerateOpts) { images.cmd.RunE = images.RunE } -func (c *GenerateImagesCmd) GenerateImages(fs *cs.FileSystem, exp exporter.Exporter) error { +func (c *GenerateImagesCmd) GenerateImages(fs *util.FileSystem, exp exporter.Exporter) error { ciInput := c.Opts.Input if c.Opts.Registry == "" { return errors.New("registry is required") diff --git a/cli/cmd/generate_images_test.go b/cli/cmd/generate_images_test.go index 01977fd..98c4952 100644 --- a/cli/cmd/generate_images_test.go +++ b/cli/cmd/generate_images_test.go @@ -13,12 +13,13 @@ import ( "github.com/codesphere-cloud/cs-go/pkg/ci" "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" + "github.com/codesphere-cloud/cs-go/pkg/util" ) var _ = Describe("GenerateImages", func() { var ( - memoryFs *cs.FileSystem - mockEnv *cmd.MockEnv + memoryFs *util.FileSystem + mockEnv *cs.MockEnv mockExporter *exporter.MockExporter c *cmd.GenerateImagesCmd wsId int @@ -28,8 +29,8 @@ var _ = Describe("GenerateImages", func() { BeforeEach(func() { input := "ci.dev.yml" - memoryFs = cs.NewMemFileSystem() - mockEnv = cmd.NewMockEnv(GinkgoT()) + memoryFs = util.NewMemFileSystem() + mockEnv = cs.NewMockEnv(GinkgoT()) mockExporter = exporter.NewMockExporter(GinkgoT()) repoRoot = "fake-root" @@ -39,10 +40,9 @@ var _ = Describe("GenerateImages", func() { c = &cmd.GenerateImagesCmd{ Opts: &cmd.GenerateImagesOpts{ GenerateOpts: &cmd.GenerateOpts{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), Input: defaultInput, Output: defaultOutput, }, diff --git a/cli/cmd/generate_kubernetes.go b/cli/cmd/generate_kubernetes.go index 87f66c7..e0d57be 100644 --- a/cli/cmd/generate_kubernetes.go +++ b/cli/cmd/generate_kubernetes.go @@ -9,9 +9,9 @@ import ( "log" "path" - "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/cs-go/pkg/util" "github.com/spf13/cobra" ) @@ -31,7 +31,7 @@ type GenerateKubernetesOpts struct { } func (c *GenerateKubernetesCmd) RunE(_ *cobra.Command, args []string) error { - fs := cs.NewOSFileSystem(c.Opts.RepoRoot) + fs := util.NewOSFileSystem(c.Opts.RepoRoot) exporter := exporter.NewExporterService(fs, c.Opts.Output, "", []string{}, c.Opts.RepoRoot, c.Opts.Force) if err := c.GenerateKubernetes(fs, exporter); err != nil { @@ -87,7 +87,7 @@ func AddGenerateKubernetesCmd(generate *cobra.Command, opts *GenerateOpts) { kubernetes.cmd.RunE = kubernetes.RunE } -func (c *GenerateKubernetesCmd) GenerateKubernetes(fs *cs.FileSystem, exp exporter.Exporter) error { +func (c *GenerateKubernetesCmd) GenerateKubernetes(fs *util.FileSystem, exp exporter.Exporter) error { ciInput := c.Opts.Input if c.Opts.Registry == "" { return errors.New("registry is required") diff --git a/cli/cmd/generate_kubernetes_test.go b/cli/cmd/generate_kubernetes_test.go index 77bd955..bcda0a5 100644 --- a/cli/cmd/generate_kubernetes_test.go +++ b/cli/cmd/generate_kubernetes_test.go @@ -12,12 +12,13 @@ import ( "github.com/codesphere-cloud/cs-go/pkg/ci" "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" + "github.com/codesphere-cloud/cs-go/pkg/util" ) var _ = Describe("GenerateKubernetes", func() { var ( - memoryFs *cs.FileSystem - mockEnv *cmd.MockEnv + memoryFs *util.FileSystem + mockEnv *cs.MockEnv mockExporter *exporter.MockExporter c *cmd.GenerateKubernetesCmd wsId int @@ -25,8 +26,8 @@ var _ = Describe("GenerateKubernetes", func() { ) BeforeEach(func() { - memoryFs = cs.NewMemFileSystem() - mockEnv = cmd.NewMockEnv(GinkgoT()) + memoryFs = util.NewMemFileSystem() + mockEnv = cs.NewMockEnv(GinkgoT()) mockExporter = exporter.NewMockExporter(GinkgoT()) repoRoot = "workspace-repo" @@ -36,10 +37,9 @@ var _ = Describe("GenerateKubernetes", func() { c = &cmd.GenerateKubernetesCmd{ Opts: &cmd.GenerateKubernetesOpts{ GenerateOpts: &cmd.GenerateOpts{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), Input: defaultInput, Output: defaultOutput, }, diff --git a/cli/cmd/list_baseimages_test.go b/cli/cmd/list_baseimages_test.go index 221ff75..fc2b2f0 100644 --- a/cli/cmd/list_baseimages_test.go +++ b/cli/cmd/list_baseimages_test.go @@ -13,6 +13,7 @@ import ( "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" ) var _ = Describe("ListBaseimagesCmd", func() { @@ -20,16 +21,14 @@ var _ = Describe("ListBaseimagesCmd", func() { c cmd.ListBaseimagesCmd globalOpts *cmd.GlobalOptions listOpts *cmd.ListOptions - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient ) BeforeEach(func() { - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) mockClient = cmd.NewMockClient(GinkgoT()) - globalOpts = &cmd.GlobalOptions{ - Env: mockEnv, - } + globalOpts = cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{}, mockEnv) listOpts = &cmd.ListOptions{ GlobalOptions: globalOpts, } @@ -114,7 +113,7 @@ var _ = Describe("AddListBaseimagesCmd", func() { BeforeEach(func() { parentCmd = &cobra.Command{Use: "list"} listOpts = &cmd.ListOptions{ - GlobalOptions: &cmd.GlobalOptions{}, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{}, cs.NewMockEnv(GinkgoT())), } }) diff --git a/cli/cmd/list_workspaces.go b/cli/cmd/list_workspaces.go index 8db191e..0620aca 100644 --- a/cli/cmd/list_workspaces.go +++ b/cli/cmd/list_workspaces.go @@ -91,7 +91,7 @@ func (l *ListWorkspacesCmd) getTeamIds(client Client) (teams []int, err error) { if err != nil { log.Println("No team ID provided via flag or environment variable, listing workspaces of all teams") } - if teamIdEnv >= 0 { + if teamIdEnv > 0 { teams = append(teams, teamIdEnv) return } diff --git a/cli/cmd/list_workspaces_test.go b/cli/cmd/list_workspaces_test.go index b38d6ec..d55c54d 100644 --- a/cli/cmd/list_workspaces_test.go +++ b/cli/cmd/list_workspaces_test.go @@ -6,6 +6,7 @@ package cmd_test import ( "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -13,22 +14,21 @@ import ( var _ = Describe("Workspace", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient l cmd.ListWorkspacesCmd teamId int ) BeforeEach(func() { - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) mockClient = cmd.NewMockClient(GinkgoT()) teamId = -1 l = cmd.ListWorkspacesCmd{ Opts: &cmd.ListOptions{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, - TeamId: -1, // force using the env mock to get a team ID - }, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ + TeamId: -1, + }, mockEnv), }, } }) @@ -39,11 +39,11 @@ var _ = Describe("Workspace", func() { Context("when team ID is set", func() { BeforeEach(func() { - teamId = 0 + teamId = 42 }) It("lists workspaces of single team", func() { - mockClient.EXPECT().ListWorkspaces(0).Return([]api.Workspace{}, nil) + mockClient.EXPECT().ListWorkspaces(42).Return([]api.Workspace{}, nil) w, err := l.ListWorkspaces(mockClient) Expect(w).To(Equal([]api.Workspace{})) diff --git a/cli/cmd/log.go b/cli/cmd/log.go index 6724ddb..088603c 100644 --- a/cli/cmd/log.go +++ b/cli/cmd/log.go @@ -108,7 +108,7 @@ func (l *LogCmd) RunE(_ *cobra.Command, args []string) (err error) { func (l *LogCmd) printAllLogs() error { log.Println("Printing logs of all replicas") - replicas, err := cs.GetPipelineStatus(l.scope.workspaceId, *l.scope.stage) + replicas, err := cs.GetPipelineStatus(l.scope.workspaceId, *l.scope.stage, l.opts.StateFile) if err != nil { return fmt.Errorf("failed to get pipeline status: %w", err) } @@ -143,7 +143,7 @@ func (l *LogCmd) printLogsOfStage() error { *l.scope.stage, *l.scope.step, ) - return printLogsOfEndpoint("", endpoint) + return l.printLogsOfEndpoint("", endpoint) } func (l *LogCmd) printLogsOfReplica(prefix string) error { @@ -154,7 +154,7 @@ func (l *LogCmd) printLogsOfReplica(prefix string) error { *l.scope.step, *l.scope.replica, ) - return printLogsOfEndpoint(prefix, endpoint) + return l.printLogsOfEndpoint(prefix, endpoint) } func (l *LogCmd) printLogsOfServer() error { @@ -165,10 +165,10 @@ func (l *LogCmd) printLogsOfServer() error { *l.scope.step, *l.scope.server, ) - return printLogsOfEndpoint("", endpoint) + return l.printLogsOfEndpoint("", endpoint) } -func printLogsOfEndpoint(prefix string, endpoint string) error { +func (l *LogCmd) printLogsOfEndpoint(prefix string, endpoint string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -179,7 +179,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 = cs.SetAuthoriziationHeader(req, l.opts.StateFile) if err != nil { return fmt.Errorf("failed to set header: %w", err) } diff --git a/cli/cmd/mocks.go b/cli/cmd/mocks.go index 42246f9..1b87bc8 100644 --- a/cli/cmd/mocks.go +++ b/cli/cmd/mocks.go @@ -1002,6 +1002,75 @@ func (_c *MockClient_WaitForWorkspaceRunning_Call) RunAndReturn(run func(workspa return _c } +// WakeUpWorkspace provides a mock function for the type MockClient +func (_mock *MockClient) WakeUpWorkspace(wsId int, token string, profile string, timeout time.Duration) error { + ret := _mock.Called(wsId, token, profile, timeout) + + if len(ret) == 0 { + panic("no return value specified for WakeUpWorkspace") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(int, string, string, time.Duration) error); ok { + r0 = returnFunc(wsId, token, profile, timeout) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockClient_WakeUpWorkspace_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WakeUpWorkspace' +type MockClient_WakeUpWorkspace_Call struct { + *mock.Call +} + +// WakeUpWorkspace is a helper method to define mock.On call +// - wsId int +// - token string +// - profile string +// - timeout time.Duration +func (_e *MockClient_Expecter) WakeUpWorkspace(wsId interface{}, token interface{}, profile interface{}, timeout interface{}) *MockClient_WakeUpWorkspace_Call { + return &MockClient_WakeUpWorkspace_Call{Call: _e.mock.On("WakeUpWorkspace", wsId, token, profile, timeout)} +} + +func (_c *MockClient_WakeUpWorkspace_Call) Run(run func(wsId int, token string, profile string, timeout time.Duration)) *MockClient_WakeUpWorkspace_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 int + if args[0] != nil { + arg0 = args[0].(int) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockClient_WakeUpWorkspace_Call) Return(err error) *MockClient_WakeUpWorkspace_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockClient_WakeUpWorkspace_Call) RunAndReturn(run func(wsId int, token string, profile string, timeout time.Duration) error) *MockClient_WakeUpWorkspace_Call { + _c.Call.Return(run) + return _c +} + // WorkspaceStatus provides a mock function for the type MockClient func (_mock *MockClient) WorkspaceStatus(workspaceId int) (*api.WorkspaceStatus, error) { ret := _mock.Called(workspaceId) @@ -1243,311 +1312,3 @@ func (_c *MockPrompt_InputPrompt_Call) RunAndReturn(run func(prompt string) stri _c.Call.Return(run) return _c } - -// NewMockBrowser creates a new instance of MockBrowser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockBrowser(t interface { - mock.TestingT - Cleanup(func()) -}) *MockBrowser { - mock := &MockBrowser{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MockBrowser is an autogenerated mock type for the Browser type -type MockBrowser struct { - mock.Mock -} - -type MockBrowser_Expecter struct { - mock *mock.Mock -} - -func (_m *MockBrowser) EXPECT() *MockBrowser_Expecter { - return &MockBrowser_Expecter{mock: &_m.Mock} -} - -// OpenIde provides a mock function for the type MockBrowser -func (_mock *MockBrowser) OpenIde(path string) error { - ret := _mock.Called(path) - - if len(ret) == 0 { - panic("no return value specified for OpenIde") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(string) error); ok { - r0 = returnFunc(path) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockBrowser_OpenIde_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenIde' -type MockBrowser_OpenIde_Call struct { - *mock.Call -} - -// OpenIde is a helper method to define mock.On call -// - path string -func (_e *MockBrowser_Expecter) OpenIde(path interface{}) *MockBrowser_OpenIde_Call { - return &MockBrowser_OpenIde_Call{Call: _e.mock.On("OpenIde", path)} -} - -func (_c *MockBrowser_OpenIde_Call) Run(run func(path string)) *MockBrowser_OpenIde_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *MockBrowser_OpenIde_Call) Return(err error) *MockBrowser_OpenIde_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockBrowser_OpenIde_Call) RunAndReturn(run func(path string) error) *MockBrowser_OpenIde_Call { - _c.Call.Return(run) - return _c -} - -// NewMockEnv creates a new instance of MockEnv. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockEnv(t interface { - mock.TestingT - Cleanup(func()) -}) *MockEnv { - mock := &MockEnv{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MockEnv is an autogenerated mock type for the Env type -type MockEnv struct { - mock.Mock -} - -type MockEnv_Expecter struct { - mock *mock.Mock -} - -func (_m *MockEnv) EXPECT() *MockEnv_Expecter { - return &MockEnv_Expecter{mock: &_m.Mock} -} - -// GetApiToken provides a mock function for the type MockEnv -func (_mock *MockEnv) GetApiToken() (string, error) { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetApiToken") - } - - var r0 string - var r1 error - if returnFunc, ok := ret.Get(0).(func() (string, error)); ok { - return returnFunc() - } - if returnFunc, ok := ret.Get(0).(func() string); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(string) - } - if returnFunc, ok := ret.Get(1).(func() error); ok { - r1 = returnFunc() - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockEnv_GetApiToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApiToken' -type MockEnv_GetApiToken_Call struct { - *mock.Call -} - -// GetApiToken is a helper method to define mock.On call -func (_e *MockEnv_Expecter) GetApiToken() *MockEnv_GetApiToken_Call { - return &MockEnv_GetApiToken_Call{Call: _e.mock.On("GetApiToken")} -} - -func (_c *MockEnv_GetApiToken_Call) Run(run func()) *MockEnv_GetApiToken_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockEnv_GetApiToken_Call) Return(s string, err error) *MockEnv_GetApiToken_Call { - _c.Call.Return(s, err) - return _c -} - -func (_c *MockEnv_GetApiToken_Call) RunAndReturn(run func() (string, error)) *MockEnv_GetApiToken_Call { - _c.Call.Return(run) - return _c -} - -// GetApiUrl provides a mock function for the type MockEnv -func (_mock *MockEnv) GetApiUrl() string { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetApiUrl") - } - - var r0 string - if returnFunc, ok := ret.Get(0).(func() string); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(string) - } - return r0 -} - -// MockEnv_GetApiUrl_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApiUrl' -type MockEnv_GetApiUrl_Call struct { - *mock.Call -} - -// GetApiUrl is a helper method to define mock.On call -func (_e *MockEnv_Expecter) GetApiUrl() *MockEnv_GetApiUrl_Call { - return &MockEnv_GetApiUrl_Call{Call: _e.mock.On("GetApiUrl")} -} - -func (_c *MockEnv_GetApiUrl_Call) Run(run func()) *MockEnv_GetApiUrl_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockEnv_GetApiUrl_Call) Return(s string) *MockEnv_GetApiUrl_Call { - _c.Call.Return(s) - return _c -} - -func (_c *MockEnv_GetApiUrl_Call) RunAndReturn(run func() string) *MockEnv_GetApiUrl_Call { - _c.Call.Return(run) - return _c -} - -// GetTeamId provides a mock function for the type MockEnv -func (_mock *MockEnv) GetTeamId() (int, error) { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetTeamId") - } - - var r0 int - var r1 error - if returnFunc, ok := ret.Get(0).(func() (int, error)); ok { - return returnFunc() - } - if returnFunc, ok := ret.Get(0).(func() int); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(int) - } - if returnFunc, ok := ret.Get(1).(func() error); ok { - r1 = returnFunc() - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockEnv_GetTeamId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTeamId' -type MockEnv_GetTeamId_Call struct { - *mock.Call -} - -// GetTeamId is a helper method to define mock.On call -func (_e *MockEnv_Expecter) GetTeamId() *MockEnv_GetTeamId_Call { - return &MockEnv_GetTeamId_Call{Call: _e.mock.On("GetTeamId")} -} - -func (_c *MockEnv_GetTeamId_Call) Run(run func()) *MockEnv_GetTeamId_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockEnv_GetTeamId_Call) Return(n int, err error) *MockEnv_GetTeamId_Call { - _c.Call.Return(n, err) - return _c -} - -func (_c *MockEnv_GetTeamId_Call) RunAndReturn(run func() (int, error)) *MockEnv_GetTeamId_Call { - _c.Call.Return(run) - return _c -} - -// GetWorkspaceId provides a mock function for the type MockEnv -func (_mock *MockEnv) GetWorkspaceId() (int, error) { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetWorkspaceId") - } - - var r0 int - var r1 error - if returnFunc, ok := ret.Get(0).(func() (int, error)); ok { - return returnFunc() - } - if returnFunc, ok := ret.Get(0).(func() int); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(int) - } - if returnFunc, ok := ret.Get(1).(func() error); ok { - r1 = returnFunc() - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockEnv_GetWorkspaceId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaceId' -type MockEnv_GetWorkspaceId_Call struct { - *mock.Call -} - -// GetWorkspaceId is a helper method to define mock.On call -func (_e *MockEnv_Expecter) GetWorkspaceId() *MockEnv_GetWorkspaceId_Call { - return &MockEnv_GetWorkspaceId_Call{Call: _e.mock.On("GetWorkspaceId")} -} - -func (_c *MockEnv_GetWorkspaceId_Call) Run(run func()) *MockEnv_GetWorkspaceId_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockEnv_GetWorkspaceId_Call) Return(n int, err error) *MockEnv_GetWorkspaceId_Call { - _c.Call.Return(n, err) - return _c -} - -func (_c *MockEnv_GetWorkspaceId_Call) RunAndReturn(run func() (int, error)) *MockEnv_GetWorkspaceId_Call { - _c.Call.Return(run) - return _c -} diff --git a/cli/cmd/open.go b/cli/cmd/open.go index cd8f417..cb974e3 100644 --- a/cli/cmd/open.go +++ b/cli/cmd/open.go @@ -12,12 +12,13 @@ import ( ) type OpenCmd struct { - cmd *cobra.Command + cmd *cobra.Command + Opts *GlobalOptions } func (c *OpenCmd) RunE(_ *cobra.Command, args []string) error { log.Println("Opening Codesphere IDE") - return cs.NewBrowser().OpenIde("") + return cs.NewBrowser().OpenIde("", c.Opts.StateFile) } func AddOpenCmd(rootCmd *cobra.Command, opts *GlobalOptions) { @@ -27,6 +28,7 @@ func AddOpenCmd(rootCmd *cobra.Command, opts *GlobalOptions) { Short: "Open the Codesphere IDE", Long: `Open the Codesphere IDE.`, }, + Opts: opts, } rootCmd.AddCommand(open.cmd) open.cmd.RunE = open.RunE diff --git a/cli/cmd/open_workspace.go b/cli/cmd/open_workspace.go index 8e2a219..35ea465 100644 --- a/cli/cmd/open_workspace.go +++ b/cli/cmd/open_workspace.go @@ -17,10 +17,6 @@ type OpenWorkspaceCmd struct { Opts *GlobalOptions } -type Browser interface { - OpenIde(path string) error -} - func (c *OpenWorkspaceCmd) RunE(_ *cobra.Command, args []string) error { client, err := NewClient(*c.Opts) if err != nil { @@ -52,7 +48,7 @@ func AddOpenWorkspaceCmd(open *cobra.Command, opts *GlobalOptions) { workspace.cmd.RunE = workspace.RunE } -func (cmd *OpenWorkspaceCmd) OpenWorkspace(browser Browser, client Client, wsId int) error { +func (cmd *OpenWorkspaceCmd) OpenWorkspace(browser cs.Browser, client Client, wsId int) error { workspace, err := client.GetWorkspace(wsId) if err != nil { return fmt.Errorf("failed to get workspace: %w", err) @@ -60,7 +56,7 @@ func (cmd *OpenWorkspaceCmd) OpenWorkspace(browser Browser, client Client, wsId log.Printf("Opening workspace %d in Codesphere IDE\n", wsId) - err = browser.OpenIde(fmt.Sprintf("teams/%d/workspaces/%d", workspace.TeamId, wsId)) + err = browser.OpenIde(fmt.Sprintf("teams/%d/workspaces/%d", workspace.TeamId, wsId), cmd.Opts.StateFile) if err != nil { return fmt.Errorf("failed to open web browser: %w", err) } diff --git a/cli/cmd/open_workspace_test.go b/cli/cmd/open_workspace_test.go index baa31b5..c7016ad 100644 --- a/cli/cmd/open_workspace_test.go +++ b/cli/cmd/open_workspace_test.go @@ -8,16 +8,18 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" ) var _ = Describe("GenerateWorkspacePath", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient - mockBrowser *cmd.MockBrowser + mockBrowser *cs.MockBrowser o *cmd.OpenWorkspaceCmd wsId int teamId int @@ -25,15 +27,14 @@ var _ = Describe("GenerateWorkspacePath", func() { JustBeforeEach(func() { mockClient = cmd.NewMockClient(GinkgoT()) - mockBrowser = cmd.NewMockBrowser(GinkgoT()) - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockBrowser = cs.NewMockBrowser(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) wsId = 42 teamId = 21 o = &cmd.OpenWorkspaceCmd{ - Opts: &cmd.GlobalOptions{ - Env: mockEnv, + Opts: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), } }) @@ -42,7 +43,7 @@ var _ = Describe("GenerateWorkspacePath", func() { Id: wsId, TeamId: teamId, }, nil) - mockBrowser.EXPECT().OpenIde(fmt.Sprintf("teams/%d/workspaces/%d", teamId, wsId)).Return(nil) + mockBrowser.EXPECT().OpenIde(fmt.Sprintf("teams/%d/workspaces/%d", teamId, wsId), mock.Anything).Return(nil) err := o.OpenWorkspace(mockBrowser, mockClient, wsId) Expect(err).ToNot(HaveOccurred()) diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go new file mode 100644 index 0000000..3e5ccd5 --- /dev/null +++ b/cli/cmd/ps.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "fmt" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/cs-go/pkg/pipeline" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" +) + +// PsCmd represents the ps command +type PsCmd struct { + cmd *cobra.Command + Opts *GlobalOptions +} + +type ServerStatus struct { + Server string `json:"server"` + ReplicaCount int `json:"state"` + ReplicaRunning int `json:"replica"` +} + +func (c *PsCmd) 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) + } + status, err := client.GetPipelineState(wsId, "run") + if err != nil { + return fmt.Errorf("failed to get pipeline state: %w", err) + } + + serverStatus := map[string]*ServerStatus{} + for _, replica := range status { + stat, ok := serverStatus[replica.Server] + if !ok { + stat = &ServerStatus{ + Server: replica.Server, + ReplicaCount: 0, + ReplicaRunning: 0, + } + serverStatus[replica.Server] = stat + } + if replica.State == "running" || replica.Server == pipeline.IdeServer { + stat.ReplicaRunning++ + } + stat.ReplicaCount++ + continue + } + + t := io.GetTableWriter() + t.AppendHeader(table.Row{"Server", "Replica (running/desired)"}) + for _, stat := range serverStatus { + t.AppendRow(table.Row{stat.Server, fmt.Sprintf("%d/%d", stat.ReplicaRunning, stat.ReplicaCount)}) + } + t.Render() + + return nil +} + +func AddPsCmd(rootCmd *cobra.Command, opts *GlobalOptions) { + ps := PsCmd{ + cmd: &cobra.Command{ + Use: "ps", + Short: "List services of a workspace", + Long: `Lists all services of a workspace with their current state.`, + }, + Opts: opts, + } + rootCmd.AddCommand(ps.cmd) + ps.cmd.RunE = ps.RunE +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index abf7d54..7bb5914 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -15,47 +15,54 @@ type GlobalOptions struct { ApiUrl string TeamId int WorkspaceId int - Env Env + env cs.Env Verbose bool + StateFile string } -type Env interface { - GetApiToken() (string, error) - GetTeamId() (int, error) - GetWorkspaceId() (int, error) - GetApiUrl() string +// NewGlobalOptionsWithCustomEnv creates a new GlobalOptions with a custom environment, useful for testing +func NewGlobalOptionsWithCustomEnv(opts GlobalOptions, env cs.Env) *GlobalOptions { + opts.env = env + return &opts +} + +func (o *GlobalOptions) Env() cs.Env { + if o.env == nil { + o.env = cs.NewEnv(o.StateFile) + } + return o.env } func (o GlobalOptions) GetApiUrl() string { if o.ApiUrl != "" { return o.ApiUrl } - return o.Env.GetApiUrl() + return o.Env().GetApiUrl() } func (o GlobalOptions) GetTeamId() (int, error) { if o.TeamId != -1 { return o.TeamId, nil } - wsId, err := o.Env.GetTeamId() + teamId, err := o.Env().GetTeamId() if err != nil { return -1, err } - if wsId < 0 { + if teamId <= 0 { return -1, errors.New("team ID not set, use -t or CS_TEAM_ID to set it") } - return wsId, nil + return teamId, nil } func (o GlobalOptions) GetWorkspaceId() (int, error) { if o.WorkspaceId != -1 { return o.WorkspaceId, nil } - wsId, err := o.Env.GetWorkspaceId() + wsId, err := o.Env().GetWorkspaceId() if err != nil { return -1, err } - if wsId < 0 { + if wsId <= 0 { return -1, errors.New("workspace ID not set, use -w or CS_WORKSPACE_ID to set it") } return wsId, nil @@ -69,12 +76,13 @@ func GetRootCmd() *cobra.Command { DisableAutoGenTag: true, } - opts := GlobalOptions{Env: cs.NewEnv()} + opts := GlobalOptions{} rootCmd.PersistentFlags().StringVarP(&opts.ApiUrl, "api", "a", "", "URL of Codesphere API (can also be CS_API)") rootCmd.PersistentFlags().IntVarP(&opts.TeamId, "team", "t", -1, "Team ID (relevant for some commands, can also be CS_TEAM_ID)") rootCmd.PersistentFlags().IntVarP(&opts.WorkspaceId, "workspace", "w", -1, "Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID)") rootCmd.PersistentFlags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Verbose output") + rootCmd.PersistentFlags().StringVarP(&opts.StateFile, "state-file", "", ".cs-up.yaml", "Path to the state file, defaults to .cs-up.yaml") AddExecCmd(rootCmd, &opts) AddLogCmd(rootCmd, &opts) @@ -95,6 +103,8 @@ func GetRootCmd() *cobra.Command { AddWakeUpCmd(rootCmd, &opts) AddCurlCmd(rootCmd, &opts) AddScaleCmd(rootCmd, &opts) + AddUpCmd(rootCmd, &opts) + AddPsCmd(rootCmd, &opts) return rootCmd } diff --git a/cli/cmd/set_env_vars_test.go b/cli/cmd/set_env_vars_test.go index 43a6852..b8aeffc 100644 --- a/cli/cmd/set_env_vars_test.go +++ b/cli/cmd/set_env_vars_test.go @@ -8,11 +8,12 @@ import ( . "github.com/onsi/gomega" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/cs" ) var _ = Describe("SetEnvVars", func() { var ( - mockEnv *cmd.MockEnv + mockEnv *cs.MockEnv mockClient *cmd.MockClient e *cmd.SetEnvVarCmd envVars []string @@ -20,15 +21,14 @@ var _ = Describe("SetEnvVars", func() { ) JustBeforeEach(func() { - mockEnv = cmd.NewMockEnv(GinkgoT()) + mockEnv = cs.NewMockEnv(GinkgoT()) mockClient = cmd.NewMockClient(GinkgoT()) wsId = 42 e = &cmd.SetEnvVarCmd{ Opts: cmd.SetEnvVarOptions{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, + GlobalOptions: cmd.NewGlobalOptionsWithCustomEnv(cmd.GlobalOptions{ WorkspaceId: wsId, - }, + }, mockEnv), EnvVar: &envVars, }, } diff --git a/cli/cmd/start_pipeline.go b/cli/cmd/start_pipeline.go index 5f3824f..4fd312e 100644 --- a/cli/cmd/start_pipeline.go +++ b/cli/cmd/start_pipeline.go @@ -5,12 +5,11 @@ package cmd import ( "fmt" - "log" - "slices" "time" "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/cs-go/pkg/pipeline" "github.com/spf13/cobra" ) @@ -27,8 +26,6 @@ type StartPipelineOpts struct { Timeout *time.Duration } -const IdeServer string = "codesphere-ide" - func (c *StartPipelineCmd) RunE(_ *cobra.Command, args []string) error { workspaceId, err := c.Opts.GetWorkspaceId() @@ -41,7 +38,15 @@ func (c *StartPipelineCmd) RunE(_ *cobra.Command, args []string) error { return fmt.Errorf("failed to create Codesphere client: %w", err) } - return c.StartPipelineStages(client, workspaceId, args) + pr := pipeline.PipelineRunner{ + Client: client, + Profile: *c.Opts.Profile, + Time: c.Time, + Timeout: *c.Opts.Timeout, + VerboseOutput: c.Opts.Verbose, + } + + return pr.StartPipelineStages(workspaceId, args) } func AddStartPipelineCmd(start *cobra.Command, opts *GlobalOptions) { @@ -78,110 +83,3 @@ func AddStartPipelineCmd(start *cobra.Command, opts *GlobalOptions) { pipeline.cmd.RunE = pipeline.RunE } - -func (c *StartPipelineCmd) StartPipelineStages(client Client, wsId int, stages []string) error { - for _, stage := range stages { - if !isValidStage(stage) { - return fmt.Errorf("invalid pipeline stage: %s", stage) - } - } - for _, stage := range stages { - err := c.startStage(client, wsId, stage) - if err != nil { - return err - } - } - return nil -} - -func isValidStage(stage string) bool { - return slices.Contains([]string{"prepare", "test", "run"}, stage) -} - -func (c *StartPipelineCmd) startStage(client Client, wsId int, stage string) error { - log.Printf("starting %s stage on workspace %d...", stage, wsId) - - err := client.StartPipelineStage(wsId, *c.Opts.Profile, stage) - if err != nil { - log.Println() - return fmt.Errorf("failed to start pipeline stage %s: %w", stage, err) - } - - err = c.waitForPipelineStage(client, wsId, stage) - if err != nil { - return fmt.Errorf("failed waiting for stage %s to finish: %w", stage, err) - - } - return nil -} - -func (c *StartPipelineCmd) waitForPipelineStage(client Client, wsId int, stage string) error { - delay := 5 * time.Second - - maxWaitTime := c.Time.Now().Add(*c.Opts.Timeout) - for { - status, err := client.GetPipelineState(wsId, stage) - if err != nil { - log.Printf("\nError getting pipeline status: %s, trying again...", err.Error()) - c.Time.Sleep(delay) - continue - } - - if c.allFinished(status) { - log.Println("(finished)") - break - } - - if allRunning(status) && stage == "run" { - log.Println("(running)") - break - } - - err = shouldAbort(status) - if err != nil { - log.Println("(failed)") - return fmt.Errorf("stage %s failed: %w", stage, err) - } - - log.Print(".") - if c.Time.Now().After(maxWaitTime) { - log.Println() - return fmt.Errorf("timed out waiting for pipeline stage %s to be complete", stage) - } - c.Time.Sleep(delay) - } - return nil -} - -func allRunning(status []api.PipelineStatus) bool { - for _, s := range status { - // Run stage is only running customer servers, ignore IDE server - if s.Server != IdeServer && s.State != "running" { - return false - } - } - return true -} - -func (c *StartPipelineCmd) allFinished(status []api.PipelineStatus) bool { - io.Verboseln(c.Opts.Verbose, "====") - for _, s := range status { - io.Verbosef(c.Opts.Verbose, "Server: %s, State: %s, Replica: %s\n", s.Server, s.State, s.Replica) - } - for _, s := range status { - // Prepare and Test stage is only running in the IDE server, ignore customer servers - if s.Server == IdeServer && s.State != "success" { - return false - } - } - return true -} - -func shouldAbort(status []api.PipelineStatus) error { - for _, s := range status { - if slices.Contains([]string{"failure", "aborted"}, s.State) { - return fmt.Errorf("server %s, replica %s reached unexpected state %s", s.Server, s.Replica, s.State) - } - } - return nil -} diff --git a/cli/cmd/up.go b/cli/cmd/up.go new file mode 100644 index 0000000..f5dec18 --- /dev/null +++ b/cli/cmd/up.go @@ -0,0 +1,122 @@ +package cmd + +import ( + "fmt" + "log" + "math/rand" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/pkg/cs" + "github.com/codesphere-cloud/cs-go/pkg/git" + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/cs-go/pkg/util" + "github.com/spf13/cobra" +) + +// UpCmd represents the up command +type UpCmd struct { + cmd *cobra.Command + Opts *UpOptions + State *cs.UpState +} + +type UpOptions struct { + *GlobalOptions + + DomainTypeString string + RepoAccessString string + Yes bool + Verbose bool +} + +func randomAdjective() string { + adjectives := []string{"amazing", "incredible", "fantastic", "wonderful", "awesome", "brilliant", "marvelous", "spectacular", "fabulous", "magnificent"} + return adjectives[rand.Intn(len(adjectives))] +} + +func randomNoun() string { + nouns := []string{"workspace", "deployment", "project", "environment", "instance", "code", "work", "app", "service", "application"} + return nouns[rand.Intn(len(nouns))] +} + +func (c *UpCmd) RunE(_ *cobra.Command, args []string) error { + log.Printf("Deploying your %s %s ...", randomAdjective(), randomNoun()) + + client, err := NewClient(*c.Opts.GlobalOptions) + if err != nil { + return fmt.Errorf("failed to create Codesphere client: %w", err) + } + + fs := util.NewOSFileSystem("./") + gitSvc := git.NewGitService(fs) + + if c.Opts.DomainTypeString != "" { + if c.Opts.DomainTypeString != string(cs.PrivateDevDomain) && c.Opts.DomainTypeString != string(cs.PublicDevDomain) { + return fmt.Errorf("invalid value for --public-dev-domain: %s, allowed values are 'public' or 'private'", c.Opts.DomainTypeString) + } + c.State.DomainType = cs.DomainType(c.Opts.DomainTypeString) + } + + if c.Opts.RepoAccessString != "" { + if c.Opts.RepoAccessString != string(cs.PublicRepo) && c.Opts.RepoAccessString != string(cs.PrivateRepo) { + return fmt.Errorf("invalid value for --private-repo: %s, allowed values are 'public' or 'private'", c.Opts.RepoAccessString) + } + c.State.RepoAccess = cs.RepoAccess(c.Opts.RepoAccessString) + } + + c.State.TeamId, err = c.Opts.GetTeamId() + if err != nil { + return fmt.Errorf("failed to get team ID: %w", err) + } + + err = c.State.Load(c.Opts.StateFile, &api.RealTime{}, fs) + if err != nil { + return fmt.Errorf("failed to load state: %w", err) + } + + err = c.State.Save() + if err != nil { + return fmt.Errorf("failed to save state: %w", err) + } + + apiToken, err := c.Opts.Env().GetApiToken() + if err != nil { + return fmt.Errorf("failed to get API token: %w", err) + } + + return cs.Up(client, gitSvc, &api.RealTime{}, fs, c.State, apiToken, c.Opts.Yes, c.Opts.Verbose) +} + +func AddUpCmd(rootCmd *cobra.Command, opts *GlobalOptions) { + up := UpCmd{ + cmd: &cobra.Command{ + Use: "up", + Short: "Deploy your local code to Codesphere", + Long: io.Long(`Deploys your local code to a new or existing Codepshere workspace. + + Prerequisite: Your code needs to be located in a git repository where you can create and push WIP branches. + When running cs up, the cs CLI will do the following: + + * Push local changes to a branch (customizable with -b), if none is specified, cs go creates a WIP branch (will be stored and reused in .cs-up.yaml) + * Create workspace if it doesn't exist yet (state in .cs-up.yaml) + * Start the deployment in Codesphere, customize which profile to use with -p flag (defaults to 'ci.yml') + * Print the dev domain of the workspace to the console once the deployment is successful`), + }, + State: &cs.UpState{}, + Opts: &UpOptions{GlobalOptions: opts}, + } + rootCmd.AddCommand(up.cmd) + up.cmd.RunE = up.RunE + up.cmd.Flags().StringVarP(&up.State.Profile, "profile", "p", "", "CI profile to use (e.g. 'ci.dev.yml' for a dev profile, you may have defined in 'ci.dev.yml'), defaults to the ci.yml profile") + up.cmd.Flags().DurationVar(&up.State.Timeout, "timeout", 0, "Timeout for the deployment process, e.g. 10m, 1h, defaults to 1m") + up.cmd.Flags().IntVarP(&up.State.Plan, "plan", "", -1, "Plan ID to use for the workspace, if not set, the first available plan will be used") + up.cmd.Flags().StringArrayVarP(&up.State.Env, "env", "e", []string{}, "Environment variables to set in the format KEY=VALUE, can be specified multiple times for multiple variables") + up.cmd.Flags().StringVarP(&up.State.Branch, "branch", "b", "", "Branch to push to, if not set, a WIP branch will be created and reused for subsequent runs") + up.cmd.Flags().StringVarP(&up.State.WorkspaceName, "workspace-name", "", "", "Name of the workspace to create, if not set, a random name will be generated") + up.cmd.Flags().StringVarP(&up.State.BaseImage, "base-image", "", "", "Base image to use for the workspace, if not set, the default base image will be used") + up.cmd.Flags().StringVarP(&up.Opts.DomainTypeString, "public-dev-domain", "", "", "Whether to create a public or private dev domain for the workspace (only applies to new workspaces), defaults to 'public'") + up.cmd.Flags().StringVarP(&up.Opts.RepoAccessString, "private-repo", "", "", "Whether the git repository is public or private (requires authentication), defaults to 'public'") + up.cmd.Flags().StringVarP(&up.State.Remote, "remote", "", "origin", "Git remote to use for pushing the code, defaults to 'origin'") + up.cmd.Flags().BoolVarP(&up.Opts.Yes, "yes", "y", false, "Skip confirmation prompt for pushing changes to the git repository") + up.cmd.Flags().BoolVarP(&up.Opts.Verbose, "verbose", "v", false, "Enable verbose output") +} diff --git a/cli/cmd/wakeup.go b/cli/cmd/wakeup.go index efac6ca..6ac25d1 100644 --- a/cli/cmd/wakeup.go +++ b/cli/cmd/wakeup.go @@ -5,8 +5,6 @@ package cmd import ( "fmt" - "log" - "net/http" "time" "github.com/codesphere-cloud/cs-go/pkg/io" @@ -15,9 +13,8 @@ import ( type WakeUpOptions struct { *GlobalOptions - Timeout time.Duration - SyncLandscape bool - Profile string + Timeout time.Duration + Profile string } type WakeUpCmd struct { @@ -36,7 +33,12 @@ func (c *WakeUpCmd) RunE(_ *cobra.Command, args []string) error { return fmt.Errorf("failed to get workspace ID: %w", err) } - return c.WakeUpWorkspace(client, wsId) + token, err := c.Opts.Env().GetApiToken() + if err != nil { + return fmt.Errorf("failed to get API token: %w", err) + } + + return client.WakeUpWorkspace(wsId, token, c.Opts.Profile, c.Opts.Timeout) } func AddWakeUpCmd(rootCmd *cobra.Command, opts *GlobalOptions) { @@ -49,8 +51,8 @@ func AddWakeUpCmd(rootCmd *cobra.Command, opts *GlobalOptions) { {Cmd: "-w 1234", Desc: "wake up workspace 1234"}, {Cmd: "", Desc: "wake up workspace set by environment variable CS_WORKSPACE_ID"}, {Cmd: "-w 1234 --timeout 60s", Desc: "wake up workspace with 60 second timeout"}, - {Cmd: "-w 1234 --sync-landscape", Desc: "wake up workspace and deploy landscape from CI profile"}, - {Cmd: "-w 1234 --sync-landscape --profile prod", Desc: "wake up workspace and deploy landscape with prod profile"}, + {Cmd: "-w 1234", Desc: "wake up workspace and deploy landscape from CI profile"}, + {Cmd: "-w 1234 --profile prod", Desc: "wake up workspace and deploy landscape with prod profile"}, }), }, Opts: WakeUpOptions{ @@ -58,110 +60,7 @@ func AddWakeUpCmd(rootCmd *cobra.Command, opts *GlobalOptions) { }, } wakeup.cmd.Flags().DurationVar(&wakeup.Opts.Timeout, "timeout", 120*time.Second, "Timeout for waking up the workspace") - wakeup.cmd.Flags().BoolVar(&wakeup.Opts.SyncLandscape, "sync-landscape", false, "Deploy landscape from CI profile after waking up") wakeup.cmd.Flags().StringVarP(&wakeup.Opts.Profile, "profile", "p", "", "CI profile to use for landscape deploy (e.g. 'prod' for ci.prod.yml)") rootCmd.AddCommand(wakeup.cmd) wakeup.cmd.RunE = wakeup.RunE } - -func (c *WakeUpCmd) WakeUpWorkspace(client Client, wsId int) error { - workspace, err := client.GetWorkspace(wsId) - if err != nil { - return fmt.Errorf("failed to get workspace: %w", err) - } - - // Check if workspace is already running - status, err := client.WorkspaceStatus(wsId) - if err != nil { - return fmt.Errorf("failed to get workspace status: %w", err) - } - - if !status.IsRunning { - log.Printf("Waking up workspace %d (%s)...\n", wsId, workspace.Name) - - // Scale workspace to at least 1 replica to wake it up - // If workspace already has replicas configured (but not running), preserve that count - targetReplicas := 1 - if workspace.Replicas > 1 { - targetReplicas = workspace.Replicas - } - - err = client.ScaleWorkspace(wsId, targetReplicas) - if err != nil { - return fmt.Errorf("failed to scale workspace: %w", err) - } - - log.Printf("Waiting for workspace %d to be running...\n", wsId) - err = client.WaitForWorkspaceRunning(&workspace, c.Opts.Timeout) - if err != nil { - return fmt.Errorf("workspace did not become running: %w", err) - } - } else { - log.Printf("Workspace %d (%s) is already running\n", wsId, workspace.Name) - } - - if c.Opts.SyncLandscape { - log.Printf("Deploying landscape for workspace %d...\n", wsId) - err = client.DeployLandscape(wsId, c.Opts.Profile) - if err != nil { - return fmt.Errorf("failed to deploy landscape: %w", err) - } - log.Printf("Landscape deployment initiated for workspace %d\n", wsId) - } - - if workspace.DevDomain == nil || *workspace.DevDomain == "" { - log.Printf("Workspace %d does not have a dev domain, skipping health check\n", wsId) - return nil - } - - log.Printf("Checking health of workspace %d (%s)...\n", wsId, workspace.Name) - - token, err := c.Opts.Env.GetApiToken() - if err != nil { - return fmt.Errorf("failed to get API token: %w", err) - } - - err = c.waitForWorkspaceHealthy(*workspace.DevDomain, token, c.Opts.Timeout) - if err != nil { - return fmt.Errorf("workspace did not become healthy: %w", err) - } - - log.Printf("Workspace %d is healthy and ready\n", wsId) - - return nil -} - -func (c *WakeUpCmd) waitForWorkspaceHealthy(devDomain string, token string, timeout time.Duration) error { - url := fmt.Sprintf("https://%s", devDomain) - delay := 5 * time.Second - maxWaitTime := time.Now().Add(timeout) - - httpClient := &http.Client{ - Timeout: 10 * time.Second, - } - - for { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("X-CS-Authorization", fmt.Sprintf("Bearer %s", token)) - - resp, err := httpClient.Do(req) - if err == nil { - _ = resp.Body.Close() - // Any HTTP response (even 502) means the workspace proxy is reachable - // and the workspace is awake. A non-200 status just means no service - // is listening on the target port yet, which is expected for fresh workspaces. - log.Printf("Workspace %s responded with status code %d\n", devDomain, resp.StatusCode) - return nil - } - - if time.Now().After(maxWaitTime) { - return fmt.Errorf("timeout waiting for workspace to be healthy at %s", url) - } - - time.Sleep(delay) - } -} diff --git a/cli/cmd/wakeup_test.go b/cli/cmd/wakeup_test.go deleted file mode 100644 index 26619d4..0000000 --- a/cli/cmd/wakeup_test.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Codesphere Inc. -// SPDX-License-Identifier: Apache-2.0 - -package cmd_test - -import ( - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/stretchr/testify/mock" - - "github.com/codesphere-cloud/cs-go/api" - "github.com/codesphere-cloud/cs-go/cli/cmd" -) - -var _ = Describe("WakeUp", func() { - var ( - mockEnv *cmd.MockEnv - mockClient *cmd.MockClient - c *cmd.WakeUpCmd - wsId int - teamId int - ) - - JustBeforeEach(func() { - mockClient = cmd.NewMockClient(GinkgoT()) - mockEnv = cmd.NewMockEnv(GinkgoT()) - wsId = 42 - teamId = 21 - c = &cmd.WakeUpCmd{ - Opts: cmd.WakeUpOptions{ - GlobalOptions: &cmd.GlobalOptions{ - Env: mockEnv, - WorkspaceId: wsId, - }, - Timeout: 120 * time.Second, - }, - } - }) - - Context("WakeUpWorkspace", func() { - It("should wake up the workspace by scaling to 1 replica", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: false}, nil) - mockClient.EXPECT().ScaleWorkspace(wsId, 1).Return(nil) - mockClient.EXPECT().WaitForWorkspaceRunning(mock.Anything, mock.Anything).Return(nil) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).ToNot(HaveOccurred()) - }) - - It("should return early if workspace is already running", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: true}, nil) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).ToNot(HaveOccurred()) - }) - - It("should return error if GetWorkspace fails", func() { - mockClient.EXPECT().GetWorkspace(wsId).Return(api.Workspace{}, fmt.Errorf("api error")) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get workspace")) - }) - - It("should return error if ScaleWorkspace fails", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: false}, nil) - mockClient.EXPECT().ScaleWorkspace(wsId, 1).Return(fmt.Errorf("scale error")) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to scale workspace")) - }) - - It("should sync landscape when SyncLandscape flag is set", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - c.Opts.SyncLandscape = true - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: false}, nil) - mockClient.EXPECT().ScaleWorkspace(wsId, 1).Return(nil) - mockClient.EXPECT().WaitForWorkspaceRunning(mock.Anything, mock.Anything).Return(nil) - mockClient.EXPECT().DeployLandscape(wsId, "").Return(nil) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).ToNot(HaveOccurred()) - }) - - It("should sync landscape with custom profile", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - c.Opts.SyncLandscape = true - c.Opts.Profile = "prod" - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: false}, nil) - mockClient.EXPECT().ScaleWorkspace(wsId, 1).Return(nil) - mockClient.EXPECT().WaitForWorkspaceRunning(mock.Anything, mock.Anything).Return(nil) - mockClient.EXPECT().DeployLandscape(wsId, "prod").Return(nil) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).ToNot(HaveOccurred()) - }) - - It("should return error if DeployLandscape fails", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - c.Opts.SyncLandscape = true - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: false}, nil) - mockClient.EXPECT().ScaleWorkspace(wsId, 1).Return(nil) - mockClient.EXPECT().WaitForWorkspaceRunning(mock.Anything, mock.Anything).Return(nil) - mockClient.EXPECT().DeployLandscape(wsId, "").Return(fmt.Errorf("deploy error")) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to deploy landscape")) - }) - - It("should sync landscape even when workspace is already running", func() { - workspace := api.Workspace{ - Id: wsId, - TeamId: teamId, - Name: "test-workspace", - } - c.Opts.SyncLandscape = true - - mockClient.EXPECT().GetWorkspace(wsId).Return(workspace, nil) - mockClient.EXPECT().WorkspaceStatus(wsId).Return(&api.WorkspaceStatus{IsRunning: true}, nil) - mockClient.EXPECT().DeployLandscape(wsId, "").Return(nil) - - err := c.WakeUpWorkspace(mockClient, wsId) - - Expect(err).ToNot(HaveOccurred()) - }) - }) -}) diff --git a/int/util/workspace.go b/int/util/workspace.go index c412126..809a638 100644 --- a/int/util/workspace.go +++ b/int/util/workspace.go @@ -113,11 +113,11 @@ func CleanupWorkspace(workspaceId string) { } } -func WaitForWorkspaceRunning(client *api.Client, workspaceId int, timeout time.Duration) error { +func WaitForWorkspaceRunning(client *api.RealClient, workspaceId int, timeout time.Duration) error { return client.WaitForWorkspaceRunning(&api.Workspace{Id: workspaceId}, timeout) } -func ScaleWorkspace(client *api.Client, workspaceId int, replicas int) error { +func ScaleWorkspace(client *api.RealClient, workspaceId int, replicas int) error { return client.ScaleWorkspace(workspaceId, replicas) } diff --git a/pkg/cs/.cs-up.yaml b/pkg/cs/.cs-up.yaml new file mode 100644 index 0000000..cd03aaf --- /dev/null +++ b/pkg/cs/.cs-up.yaml @@ -0,0 +1,12 @@ +profile: "" +timeout: 0s +branch: "" +team: 30 +workspace: 42 +plan: 8 +workspace_name: "" +base_image: "" +env: [] +public_dev_domain: public +repo_access: public +remote: origin diff --git a/pkg/cs/browser.go b/pkg/cs/browser.go index 2ea3058..6d162ec 100644 --- a/pkg/cs/browser.go +++ b/pkg/cs/browser.go @@ -12,15 +12,19 @@ import ( "strings" ) -type Browser struct{} +type Browser interface { + OpenIde(path string, statefile string) error +} + +type RealBrowser struct{} -func NewBrowser() *Browser { - return &Browser{} +func NewBrowser() Browser { + return &RealBrowser{} } -func (b *Browser) OpenIde(path string) error { +func (b *RealBrowser) OpenIde(path string, statefile string) error { re := regexp.MustCompile(`/api`) - ideUrl := re.ReplaceAllString(NewEnv().GetApiUrl(), "/ide") + ideUrl := re.ReplaceAllString(NewEnv(statefile).GetApiUrl(), "/ide") if !strings.HasPrefix(path, "/") { ideUrl += "/" } diff --git a/pkg/cs/env.go b/pkg/cs/env.go index 032cf50..ef077b3 100644 --- a/pkg/cs/env.go +++ b/pkg/cs/env.go @@ -8,13 +8,25 @@ import ( "fmt" "os" "strconv" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/pkg/util" ) +type Env interface { + GetApiToken() (string, error) + GetTeamId() (int, error) + GetWorkspaceId() (int, error) + GetApiUrl() string +} type Environment struct { + statefile string } -func NewEnv() *Environment { - return &Environment{} +func NewEnv(statefile string) *Environment { + return &Environment{ + statefile: statefile, + } } func (e *Environment) GetApiToken() (string, error) { @@ -31,10 +43,23 @@ func (e *Environment) GetWorkspaceId() (int, error) { return prefixedId, nil } + upState := &UpState{} + err = upState.Load(e.statefile, &api.RealTime{}, util.NewOSFileSystem(".")) + + if err == nil && upState.WorkspaceId > -1 { + return upState.WorkspaceId, nil + } + return e.ReadNumericEnv("WORKSPACE_ID") } func (e *Environment) GetTeamId() (int, error) { + upState := &UpState{} + err := upState.Load(e.statefile, &api.RealTime{}, util.NewOSFileSystem(".")) + + if err == nil && upState.TeamId > 0 { + return upState.TeamId, nil + } return e.ReadNumericEnv("CS_TEAM_ID") } diff --git a/pkg/cs/mocks.go b/pkg/cs/mocks.go new file mode 100644 index 0000000..8ede8c4 --- /dev/null +++ b/pkg/cs/mocks.go @@ -0,0 +1,323 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package cs + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewMockBrowser creates a new instance of MockBrowser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockBrowser(t interface { + mock.TestingT + Cleanup(func()) +}) *MockBrowser { + mock := &MockBrowser{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockBrowser is an autogenerated mock type for the Browser type +type MockBrowser struct { + mock.Mock +} + +type MockBrowser_Expecter struct { + mock *mock.Mock +} + +func (_m *MockBrowser) EXPECT() *MockBrowser_Expecter { + return &MockBrowser_Expecter{mock: &_m.Mock} +} + +// OpenIde provides a mock function for the type MockBrowser +func (_mock *MockBrowser) OpenIde(path string, statefile string) error { + ret := _mock.Called(path, statefile) + + if len(ret) == 0 { + panic("no return value specified for OpenIde") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { + r0 = returnFunc(path, statefile) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockBrowser_OpenIde_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenIde' +type MockBrowser_OpenIde_Call struct { + *mock.Call +} + +// OpenIde is a helper method to define mock.On call +// - path string +// - statefile string +func (_e *MockBrowser_Expecter) OpenIde(path interface{}, statefile interface{}) *MockBrowser_OpenIde_Call { + return &MockBrowser_OpenIde_Call{Call: _e.mock.On("OpenIde", path, statefile)} +} + +func (_c *MockBrowser_OpenIde_Call) Run(run func(path string, statefile string)) *MockBrowser_OpenIde_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockBrowser_OpenIde_Call) Return(err error) *MockBrowser_OpenIde_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockBrowser_OpenIde_Call) RunAndReturn(run func(path string, statefile string) error) *MockBrowser_OpenIde_Call { + _c.Call.Return(run) + return _c +} + +// NewMockEnv creates a new instance of MockEnv. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockEnv(t interface { + mock.TestingT + Cleanup(func()) +}) *MockEnv { + mock := &MockEnv{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockEnv is an autogenerated mock type for the Env type +type MockEnv struct { + mock.Mock +} + +type MockEnv_Expecter struct { + mock *mock.Mock +} + +func (_m *MockEnv) EXPECT() *MockEnv_Expecter { + return &MockEnv_Expecter{mock: &_m.Mock} +} + +// GetApiToken provides a mock function for the type MockEnv +func (_mock *MockEnv) GetApiToken() (string, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetApiToken") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func() (string, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockEnv_GetApiToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApiToken' +type MockEnv_GetApiToken_Call struct { + *mock.Call +} + +// GetApiToken is a helper method to define mock.On call +func (_e *MockEnv_Expecter) GetApiToken() *MockEnv_GetApiToken_Call { + return &MockEnv_GetApiToken_Call{Call: _e.mock.On("GetApiToken")} +} + +func (_c *MockEnv_GetApiToken_Call) Run(run func()) *MockEnv_GetApiToken_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockEnv_GetApiToken_Call) Return(s string, err error) *MockEnv_GetApiToken_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockEnv_GetApiToken_Call) RunAndReturn(run func() (string, error)) *MockEnv_GetApiToken_Call { + _c.Call.Return(run) + return _c +} + +// GetApiUrl provides a mock function for the type MockEnv +func (_mock *MockEnv) GetApiUrl() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetApiUrl") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// MockEnv_GetApiUrl_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApiUrl' +type MockEnv_GetApiUrl_Call struct { + *mock.Call +} + +// GetApiUrl is a helper method to define mock.On call +func (_e *MockEnv_Expecter) GetApiUrl() *MockEnv_GetApiUrl_Call { + return &MockEnv_GetApiUrl_Call{Call: _e.mock.On("GetApiUrl")} +} + +func (_c *MockEnv_GetApiUrl_Call) Run(run func()) *MockEnv_GetApiUrl_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockEnv_GetApiUrl_Call) Return(s string) *MockEnv_GetApiUrl_Call { + _c.Call.Return(s) + return _c +} + +func (_c *MockEnv_GetApiUrl_Call) RunAndReturn(run func() string) *MockEnv_GetApiUrl_Call { + _c.Call.Return(run) + return _c +} + +// GetTeamId provides a mock function for the type MockEnv +func (_mock *MockEnv) GetTeamId() (int, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetTeamId") + } + + var r0 int + var r1 error + if returnFunc, ok := ret.Get(0).(func() (int, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() int); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(int) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockEnv_GetTeamId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTeamId' +type MockEnv_GetTeamId_Call struct { + *mock.Call +} + +// GetTeamId is a helper method to define mock.On call +func (_e *MockEnv_Expecter) GetTeamId() *MockEnv_GetTeamId_Call { + return &MockEnv_GetTeamId_Call{Call: _e.mock.On("GetTeamId")} +} + +func (_c *MockEnv_GetTeamId_Call) Run(run func()) *MockEnv_GetTeamId_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockEnv_GetTeamId_Call) Return(n int, err error) *MockEnv_GetTeamId_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockEnv_GetTeamId_Call) RunAndReturn(run func() (int, error)) *MockEnv_GetTeamId_Call { + _c.Call.Return(run) + return _c +} + +// GetWorkspaceId provides a mock function for the type MockEnv +func (_mock *MockEnv) GetWorkspaceId() (int, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetWorkspaceId") + } + + var r0 int + var r1 error + if returnFunc, ok := ret.Get(0).(func() (int, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() int); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(int) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockEnv_GetWorkspaceId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWorkspaceId' +type MockEnv_GetWorkspaceId_Call struct { + *mock.Call +} + +// GetWorkspaceId is a helper method to define mock.On call +func (_e *MockEnv_Expecter) GetWorkspaceId() *MockEnv_GetWorkspaceId_Call { + return &MockEnv_GetWorkspaceId_Call{Call: _e.mock.On("GetWorkspaceId")} +} + +func (_c *MockEnv_GetWorkspaceId_Call) Run(run func()) *MockEnv_GetWorkspaceId_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockEnv_GetWorkspaceId_Call) Return(n int, err error) *MockEnv_GetWorkspaceId_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *MockEnv_GetWorkspaceId_Call) RunAndReturn(run func() (int, error)) *MockEnv_GetWorkspaceId_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/cs/state.go b/pkg/cs/state.go new file mode 100644 index 0000000..bd3b18a --- /dev/null +++ b/pkg/cs/state.go @@ -0,0 +1,134 @@ +package cs + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/pkg/util" + "go.yaml.in/yaml/v2" +) + +type DomainType string + +const ( + PrivateDevDomain DomainType = "private" + PublicDevDomain DomainType = "public" +) + +type RepoAccess string + +const ( + PublicRepo RepoAccess = "public" + PrivateRepo RepoAccess = "private" +) + +// UpState represents the state of the up command that is stored in .cs-up.yaml to be reused for subsequent runs. +type UpState struct { + Profile string + Timeout time.Duration + Branch string `yaml:"branch"` + TeamId int `yaml:"team"` + WorkspaceId int `yaml:"workspace"` + Plan int `yaml:"plan"` + WorkspaceName string `yaml:"workspace_name"` + BaseImage string `yaml:"base_image"` + Env []string `yaml:"env"` + DomainType DomainType `yaml:"public_dev_domain"` + RepoAccess RepoAccess `yaml:"repo_access"` + Remote string `yaml:"remote"` + + StateFile string `yaml:"-"` + fs *util.FileSystem +} + +// Writes .cs-up.yaml +func (s *UpState) Save() error { + log.Printf("Saving state to %s", s.StateFile) + output, err := yaml.Marshal(s) + if err != nil { + return fmt.Errorf("failed to marshal state: %w", err) + } + err = s.fs.WriteFile("", s.StateFile, output, true) + if err != nil { + return fmt.Errorf("failed to write state file: %w", err) + } + return nil +} + +// Load loads the state from local file. If the file doesn't exist, it returns an empty state. +// LoadState only sets properties that are not already set to allow overriding with flags. +// For example, if the user specifies a profile with the --profile flag, it will not be overridden by the value in the state file. +// If the state is updated by the flags provided, the updated state will be saved to the file after the deployment is successful. +func (s *UpState) Load(filename string, t api.Time, fs *util.FileSystem) error { + s.StateFile = filename + s.fs = fs + + branchName := fmt.Sprintf("cs-up-%s", t.Now().Format("20060102150405")) + newState := &UpState{ + DomainType: PublicDevDomain, + RepoAccess: PublicRepo, + WorkspaceName: branchName, + Remote: "origin", + Profile: "ci.yml", + Branch: branchName, + Timeout: 5 * time.Minute, + TeamId: -1, + WorkspaceId: -1, + Plan: -1, + } + _, err := fs.Stat(filename) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to stat state file: %w", err) + } + if err == nil { + data, err := fs.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read state file: %w", err) + } + err = yaml.Unmarshal(data, newState) + if err != nil { + return fmt.Errorf("failed to unmarshal state: %w", err) + } + } + + if s.Profile == "" { + s.Profile = newState.Profile + } + if s.Timeout == 0 { + s.Timeout = newState.Timeout + } + if s.Branch == "" { + s.Branch = newState.Branch + } + if s.WorkspaceId <= 0 { + s.WorkspaceId = newState.WorkspaceId + } + if s.Plan <= 0 { + s.Plan = newState.Plan + } + if s.WorkspaceName == "" { + s.WorkspaceName = newState.WorkspaceName + } + if len(s.Env) == 0 { + s.Env = newState.Env + } + if s.BaseImage == "" { + s.BaseImage = newState.BaseImage + } + if s.DomainType == "" { + s.DomainType = newState.DomainType + } + if s.RepoAccess == "" { + s.RepoAccess = newState.RepoAccess + } + if s.Remote == "" { + s.Remote = newState.Remote + } + if s.TeamId <= 0 { + s.TeamId = newState.TeamId + } + return nil +} diff --git a/pkg/cs/up.go b/pkg/cs/up.go new file mode 100644 index 0000000..80f9dab --- /dev/null +++ b/pkg/cs/up.go @@ -0,0 +1,337 @@ +package cs + +import ( + "fmt" + "log" + "slices" + "strings" + "time" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/api/errors" + "github.com/codesphere-cloud/cs-go/pkg/git" + "github.com/codesphere-cloud/cs-go/pkg/pipeline" + "github.com/codesphere-cloud/cs-go/pkg/util" +) + +type CodesphereDeploymentManager struct { + Client api.Client + GitSvc git.Git + FileSys *util.FileSystem + State *UpState + Verbose bool + AskConfirmation bool + ApiToken string + Time api.Time +} + +// Up deploys the code to Codesphere. It checks if the workspace already exists, if yes, it wakes it up and deploys the latest code changes, +// if not, it creates a new workspace and deploys the code. +// The workspace ID and other state is stored in .cs-up.yaml to be reused for subsequent runs. +func Up(client api.Client, gitSvc git.Git, time api.Time, fs *util.FileSystem, state *UpState, apiToken string, yes bool, verbose bool) error { + mgr := &CodesphereDeploymentManager{ + Client: client, + GitSvc: gitSvc, + FileSys: fs, + State: state, + Verbose: verbose, + AskConfirmation: !yes, + ApiToken: apiToken, + Time: time, + } + + err := mgr.UpdateGitIgnore() + if err != nil { + return fmt.Errorf("failed to update .gitignore: %w", err) + } + + // Push code to branch + log.Printf("Pushing code to branch %s repository...", state.Branch) + err = mgr.PushChanges(!yes) + if err != nil { + return fmt.Errorf("failed to push branch: %w", err) + } + + err = mgr.EnsureWorkspace() + if err != nil { + return fmt.Errorf("failed to ensure workspace: %w", err) + } + + // Pull branch in workspace to update the workspace with the latest changes. + // This is required in case the branch was created in a previous run and already exists in the remote repository. + err = client.GitPull(state.WorkspaceId, state.Remote, state.Branch) + if err != nil { + return fmt.Errorf("failed to pull branch: %w", err) + } + + err = mgr.DeployChanges() + if err != nil { + return fmt.Errorf("failed to deploy changes: %w", err) + } + + pr := pipeline.NewPipelineRunner(client, state.Profile, state.Timeout, verbose) + err = pr.StartPipelineStages(state.WorkspaceId, []string{"prepare", "test", "run"}) + if err != nil { + return fmt.Errorf("failed to start pipeline stages: %w", err) + } + + ws, err := client.GetWorkspace(state.WorkspaceId) + if err != nil { + return fmt.Errorf("failed to get workspace: %w", err) + } + devDomain := "not available" + if ws.DevDomain != nil { + devDomain = *ws.DevDomain + } + log.Printf("Workspace %d deployed successfully! Dev domain: %s\n", state.WorkspaceId, devDomain) + + return nil +} + +// getPlan returns the plan ID to use for the workspace. If a plan ID is already set in the state, +// it returns that, otherwise it fetches the available plans from the API and returns the first one. +func (c *CodesphereDeploymentManager) getPlan() (int, error) { + plan := c.State.Plan + if plan == -1 { + plans, err := c.Client.ListWorkspacePlans() + if err != nil { + return -1, fmt.Errorf("failed to list plans: %w", err) + } + if len(plans) == 0 { + return -1, fmt.Errorf("no plans available for deployment") + } + c.State.Plan = plans[0].Id + } + return c.State.Plan, nil +} + +// UpdateGitIgnore adds .cs-up.yaml to .gitignore if it doesn't exist to avoid committing the state file +func (c *CodesphereDeploymentManager) UpdateGitIgnore() error { + // Add .cs-up.yaml to .gitignore if it doesn't exist to avoid committing the state file + if c.FileSys.FileExists(".gitignore") { + content, err := c.FileSys.ReadFile(".gitignore") + if err != nil { + return fmt.Errorf("failed to read .gitignore: %w", err) + } + if !strings.Contains(string(content), ".cs-up.yaml") { + content = append(content, []byte("\n.cs-up.yaml\n")...) + err = c.FileSys.WriteFile("./", ".gitignore", content, true) + if err != nil { + return fmt.Errorf("failed to write .gitignore: %w", err) + } + } + return nil + } + err := c.FileSys.WriteFile("./", ".gitignore", []byte(".cs-up.yaml\n"), true) + if err != nil { + return fmt.Errorf("failed to create .gitignore: %w", err) + } + return nil +} + +// PushChanges pushes the local changes to the remote branch. If the branch doesn't exist, it will be created. +func (c *CodesphereDeploymentManager) PushChanges(askConfirmation bool) error { + // checkout branhch using git CLI, if it doesn't exist create it + err := c.GitSvc.Checkout(c.State.Branch, false) + if err != nil { + err = c.GitSvc.Checkout(c.State.Branch, true) + if err != nil { + return fmt.Errorf("failed to checkout branch: %w", err) + } + + err = c.GitSvc.Push(c.State.Remote, c.State.Branch) + if err != nil { + return fmt.Errorf("failed to push branch: %w", err) + } + } + if err != nil { + return fmt.Errorf("failed to checkout branch: %w", err) + } + + // use git cli to add all files + err = c.GitSvc.AddAll() + if err != nil { + return fmt.Errorf("failed to add files: %w", err) + } + + // if nothing changed compared to the remote branch, skip commit and push + hasChanges, err := c.GitSvc.HasChanges(c.State.Remote, c.State.Branch) + if err != nil { + return fmt.Errorf("failed to check for changes compared to remote: %w", err) + } + if !hasChanges { + log.Printf("No changes to push to remote branch %s, skipping push\n", c.State.Branch) + return nil + } + + // print files being added and ask for confirmation before pushing + err = c.GitSvc.PrintStatus() + if err != nil { + return fmt.Errorf("failed to get status: %w", err) + } + + if askConfirmation { + fmt.Print("Do you want to push these changes? (y/n): ") + var response string + _, err = fmt.Scanln(&response) + if err != nil { + return fmt.Errorf("failed to read input: %w", err) + } + if strings.ToLower(response) != "y" { + return fmt.Errorf("aborting push") + } + } + + err = c.GitSvc.Commit("cs up commit") + if err != nil { + return fmt.Errorf("failed to commit changes: %w", err) + } + + err = c.GitSvc.Push(c.State.Remote, c.State.Branch) + if err != nil { + return fmt.Errorf("failed to push changes: %w", err) + } + + return nil +} + +// EnsureWorkspace checks if the workspace already exists, if yes, it wakes it up, if not, it creates a new workspace. +// It also sets the environment variables on the workspace. +func (c *CodesphereDeploymentManager) EnsureWorkspace() error { + envVars, err := ArgToEnvVarMap(c.State.Env) + if err != nil { + return fmt.Errorf("failed to parse environment variables: %w", err) + } + + workspaceExists, err := c.wakeupWorkspaceIfExists(envVars) + if err != nil { + return fmt.Errorf("failed to wake up workspace: %w", err) + } + + if c.State.WorkspaceId > 0 && workspaceExists { + return nil + } + + wsId, err := c.createWorkspace(envVars) + if err != nil { + return fmt.Errorf("failed to create workspace: %w", err) + } + + c.State.WorkspaceId = wsId + err = c.State.Save() + if err != nil { + return fmt.Errorf("failed to save state: %w", err) + } + + log.Println("Your workspace is being deployed.") + return nil +} + +// WakeupWorkspaceIfExists checks if the workspace already exists, if yes, it wakes it up and sets the environment variables, +// returns true if the workspace was woken up, false if the workspace does not exist +func (c *CodesphereDeploymentManager) wakeupWorkspaceIfExists(envVars map[string]string) (bool, error) { + if c.State.WorkspaceId <= 0 { + return false, nil + } + + log.Printf("Checking workspace with ID %d...\n", c.State.WorkspaceId) + + _, err := c.Client.GetWorkspace(c.State.WorkspaceId) + if err != nil && !errors.IsNotFound(err) { + return false, fmt.Errorf("failed to get workspace: %w", err) + } + if err != nil && errors.IsNotFound(err) { + log.Printf("Workspace with ID %d does not exist.\n", c.State.WorkspaceId) + return false, nil + } + + err = c.Client.WakeUpWorkspace(c.State.WorkspaceId, c.ApiToken, c.State.Profile, c.State.Timeout) + if err != nil { + return false, fmt.Errorf("failed to wake up workspace: %w", err) + } + + err = c.Client.SetEnvVarOnWorkspace(c.State.WorkspaceId, envVars) + if err != nil { + return false, fmt.Errorf("failed to set environment variables on workspace: %w", err) + } + return true, nil +} + +// CreateWorkspace creates a new workspace with the specified configuration and returns the workspace ID. +func (c *CodesphereDeploymentManager) createWorkspace(envVars map[string]string) (int, error) { + plan, err := c.getPlan() + if err != nil { + return -1, fmt.Errorf("failed to get plan: %w", err) + } + log.Println("Creating workspace ...") + restricted := c.State.DomainType == PrivateDevDomain + args := api.DeployWorkspaceArgs{ + TeamId: c.State.TeamId, + PlanId: plan, + Name: c.State.WorkspaceName, + EnvVars: envVars, + + IsPrivateRepo: c.State.RepoAccess == PrivateRepo, + Restricted: &restricted, + + Timeout: c.State.Timeout, + } + + remoteUrl, err := c.GitSvc.GetRemoteUrl(c.State.Remote) + if err != nil { + return -1, fmt.Errorf("failed to get remote URL: %w", err) + } + + validatedUrl, err := ValidateUrl(remoteUrl) + if err != nil { + return -1, fmt.Errorf("validation of repository URL failed: %w", err) + } + args.GitUrl = &validatedUrl + + if c.State.Branch != "" { + args.Branch = &c.State.Branch + } + + if c.State.BaseImage != "" { + baseimages, err := c.Client.ListBaseimages() + if err != nil { + return -1, fmt.Errorf("failed to list base images: %w", err) + } + + baseimageNames := make([]string, len(baseimages)) + for i, bi := range baseimages { + baseimageNames[i] = bi.GetId() + } + + if !slices.Contains(baseimageNames, c.State.BaseImage) { + return -1, fmt.Errorf("base image '%s' not found, available options are: %s", c.State.BaseImage, strings.Join(baseimageNames, ", ")) + } + + args.BaseImage = &c.State.BaseImage + } + + ws, err := c.Client.DeployWorkspace(args) + if err != nil { + return -1, fmt.Errorf("failed to create workspace: %w", err) + } + return ws.Id, nil +} + +// DeployChanges deploys the latest code changes to the workspace. +// It retries the deployment up to 3 times in case of a server error (500). +func (c *CodesphereDeploymentManager) DeployChanges() error { + // retrying the deployment in case it fails a 500 + for retries := 0; retries < 3; retries++ { + err := c.Client.DeployLandscape(c.State.WorkspaceId, c.State.Profile) + if err != nil { + if strings.Contains(err.Error(), "500") { + log.Printf("Deployment failed with a server error, retrying... (%d/3)\n", retries+1) + c.Time.Sleep(5 * time.Second) + continue + } + return fmt.Errorf("failed to deploy landscape: %w", err) + } + break + } + return nil +} diff --git a/pkg/cs/up_test.go b/pkg/cs/up_test.go new file mode 100644 index 0000000..e1732d2 --- /dev/null +++ b/pkg/cs/up_test.go @@ -0,0 +1,422 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cs_test + +import ( + "fmt" + "os" + "time" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/pkg/cs" + "github.com/codesphere-cloud/cs-go/pkg/git" + "github.com/codesphere-cloud/cs-go/pkg/testutil" + "github.com/codesphere-cloud/cs-go/pkg/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" +) + +var ( + fakeTime *api.MockTime +) +var _ = Describe("cs up deployment", func() { + var ( + state *cs.UpState + ask bool + token string + fs *util.FileSystem + mockClient *api.MockClient + mockGit *git.MockGit + mockTime *api.MockTime + devDomain string + wsName string + branchName string + remote string + ) + BeforeEach(func() { + mockClient = api.NewMockClient(GinkgoT()) + mockGit = git.NewMockGit(GinkgoT()) + mockTime = testutil.MockTime() + fs = util.NewMemFileSystem() + ask = false + token = "fake-token" + fakeTime = testutil.MockTimeAt(time.Date(2026, 01, 01, 11, 00, 00, 00, time.UTC)) + devDomain = "cs-up-20260101110000-cs-dev.codesphere.com" + wsName = "cs-up-20260101110000" + branchName = wsName + remote = "origin" + + // Default state for tests, can be overridden in specific contexts + state = &cs.UpState{ + TeamId: 30, + RepoAccess: cs.PublicRepo, + DomainType: cs.PublicDevDomain, + Env: []string{}, + Remote: remote, + Plan: -1, + Profile: "", + } + + // initialize state with defaults and save to file. This is done by the cmd before calling cs.Up + err := state.Load(".cs-up.yaml", fakeTime, fs) + Expect(err).ToNot(HaveOccurred()) + err = state.Save() + Expect(err).ToNot(HaveOccurred()) + }) + AfterEach(func() { + mockClient.AssertExpectations(GinkgoT()) + mockGit.AssertExpectations(GinkgoT()) + fakeTime.AssertExpectations(GinkgoT()) + }) + Context("Up", func() { + JustBeforeEach(func() { + mockGit.EXPECT().Checkout(branchName, false).Return(nil) + mockGit.EXPECT().AddAll().Return(nil) + mockGit.EXPECT().HasChanges("origin", branchName).Return(false, nil) + + mockClient.EXPECT().GitPull(42, "origin", branchName).Return(nil) + + mockClient.EXPECT().StartPipelineStage(42, "ci.yml", "prepare").Return(nil) + mockClient.EXPECT().GetPipelineState(42, "prepare").Return([]api.PipelineStatus{ + {Server: "codesphere-ide", State: "success", Replica: "1"}, + {Server: "backend", State: "success", Replica: "1"}, + }, nil) + + mockClient.EXPECT().StartPipelineStage(42, "ci.yml", "test").Return(nil) + mockClient.EXPECT().GetPipelineState(42, "test").Return([]api.PipelineStatus{ + {Server: "codesphere-ide", State: "success", Replica: "1"}, + {Server: "backend", State: "success", Replica: "1"}, + }, nil) + + mockClient.EXPECT().StartPipelineStage(42, "ci.yml", "run").Return(nil) + mockClient.EXPECT().GetPipelineState(42, "run").Return([]api.PipelineStatus{ + {Server: "codesphere-ide", State: "success", Replica: "1"}, + {Server: "backend", State: "running", Replica: "1"}, + }, nil) + + mockClient.EXPECT().GetWorkspace(42).Return(api.Workspace{ + Id: 42, + Name: wsName, + DevDomain: &devDomain, + }, nil) + }) + Context("default parameters", func() { + Context("when the workspace doesn't exist", func() { + It("creates a new workspace", func() { + mockGit.EXPECT().GetRemoteUrl("origin").Return("https://myrepo.git", nil) + + mockClient.EXPECT().ListWorkspacePlans().Return([]api.WorkspacePlan{{Id: 8}}, nil) + mockClient.EXPECT().DeployWorkspace(mock.Anything).Return(&api.Workspace{ + Id: 42, + Name: wsName, + }, nil) + mockClient.EXPECT().DeployLandscape(42, "ci.yml").Return(nil) + + err := cs.Up(mockClient, mockGit, mockTime, fs, state, token, ask, false) + Expect(err).To(Not(HaveOccurred())) + + validateState(fs, &cs.UpState{ + WorkspaceId: 42, + WorkspaceName: wsName, + Profile: "ci.yml", + Timeout: state.Timeout, + Branch: wsName, + TeamId: 30, + Plan: 8, + BaseImage: "", + Env: []string{}, + DomainType: cs.PublicDevDomain, + RepoAccess: cs.PublicRepo, + Remote: remote, + StateFile: ".cs-up.yaml", + }) + }) + }) + Context("when the workspace already exists", func() { + JustBeforeEach(func() { + state.Plan = 8 + state.WorkspaceId = 42 + state.WorkspaceName = wsName + state.Branch = branchName + state.Profile = "ci.yml" + err := state.Save() + Expect(err).ToNot(HaveOccurred()) + + mockClient.EXPECT().GetWorkspace(42).Return(api.Workspace{ + Id: 42, + Name: wsName, + }, nil) + + // Workspace exists, so wake it up instead of creating new + mockClient.EXPECT().WakeUpWorkspace(42, token, "ci.yml", state.Timeout).Return(nil) + mockClient.EXPECT().SetEnvVarOnWorkspace(42, mock.Anything).Return(nil) + }) + It("reuses the existing workspace", func() { + mockClient.EXPECT().DeployLandscape(42, "ci.yml").Return(nil) + err := cs.Up(mockClient, mockGit, mockTime, fs, state, token, ask, false) + Expect(err).To(Not(HaveOccurred())) + + validateState(fs, &cs.UpState{ + WorkspaceId: 42, + WorkspaceName: wsName, + Profile: "ci.yml", + Timeout: state.Timeout, + Branch: wsName, + TeamId: 30, + Plan: 8, + BaseImage: "", + Env: []string{}, + DomainType: cs.PublicDevDomain, + RepoAccess: cs.PublicRepo, + Remote: remote, + StateFile: ".cs-up.yaml", + }) + }) + Context("when the branch name provided", func() { + BeforeEach(func() { + branchName = "my-feature-branch" + }) + It("uses the provided branch name instead of the generated one", func() { + mockClient.EXPECT().DeployLandscape(42, "ci.yml").Return(nil) + + err := cs.Up(mockClient, mockGit, mockTime, fs, state, token, ask, false) + Expect(err).To(Not(HaveOccurred())) + + validateState(fs, &cs.UpState{ + WorkspaceId: 42, + WorkspaceName: wsName, + Profile: "ci.yml", + Timeout: state.Timeout, + Branch: branchName, + TeamId: 30, + Plan: 8, + BaseImage: "", + Env: []string{}, + DomainType: cs.PublicDevDomain, + RepoAccess: cs.PublicRepo, + Remote: remote, + StateFile: ".cs-up.yaml", + }) + }) + + }) + Context("when the deployment request fails the first time", func() { + It("should retry waking up the workspace", func() { + mockClient.EXPECT().DeployLandscape(42, "ci.yml").Return(fmt.Errorf("deployment error 500")).Once() + mockClient.EXPECT().DeployLandscape(42, "ci.yml").Return(nil).Once() + err := cs.Up(mockClient, mockGit, mockTime, fs, state, token, ask, false) + Expect(err).To(Not(HaveOccurred())) + + validateState(fs, &cs.UpState{ + WorkspaceId: 42, + WorkspaceName: wsName, + Profile: "ci.yml", + Timeout: state.Timeout, + Branch: branchName, + TeamId: 30, + Plan: 8, + BaseImage: "", + Env: []string{}, + DomainType: cs.PublicDevDomain, + RepoAccess: cs.PublicRepo, + Remote: remote, + StateFile: ".cs-up.yaml", + }) + + Expect(err).ToNot(HaveOccurred()) + }) + + }) + }) + }) + }) + + Describe("CodesphereDeploymentManager", func() { + var ( + mgr *cs.CodesphereDeploymentManager + ) + JustBeforeEach(func() { + mgr = &cs.CodesphereDeploymentManager{ + Client: mockClient, + GitSvc: mockGit, + FileSys: fs, + State: state, + Verbose: false, + AskConfirmation: ask, + ApiToken: token, + } + }) + + Describe("UpdateGitIgnore", func() { + It("should add .cs-up.yaml to .gitignore if it doesn't exist", func() { + err := mgr.UpdateGitIgnore() + Expect(err).ToNot(HaveOccurred()) + + // Verify that .cs-up.yaml is added to .gitignore + gitIgnoreFile, err := fs.OpenFile(".gitignore", os.O_RDWR|os.O_CREATE, 0644) + Expect(err).ToNot(HaveOccurred()) + content, err := fs.ReadFile(".gitignore") + Expect(err).ToNot(HaveOccurred()) + _ = gitIgnoreFile.Close() + + Expect(string(content)).To(ContainSubstring(".cs-up.yaml")) + + Expect(err).ToNot(HaveOccurred()) + }) + It("should not add .cs-up.yaml to .gitignore if it already exists", func() { + // Simulate .cs-up.yaml already being in .gitignore + content := []byte("other\nstuff\n.cs-up.yaml\nignored\n") + _, err := fs.CreateFile(".gitignore") + Expect(err).ToNot(HaveOccurred()) + gitIgnoreFile, err := fs.OpenFile(".gitignore", os.O_RDWR|os.O_CREATE, 0644) + Expect(err).ToNot(HaveOccurred()) + _, err = gitIgnoreFile.Write(content) + Expect(err).ToNot(HaveOccurred()) + _ = gitIgnoreFile.Close() + + err = mgr.UpdateGitIgnore() + Expect(err).ToNot(HaveOccurred()) + + // Verify that .cs-up.yaml is not duplicated in .gitignore + gitIgnoreFile, err = fs.OpenFile(".gitignore", os.O_RDONLY, 0644) + Expect(err).ToNot(HaveOccurred()) + newContent, err := fs.ReadFile(".gitignore") + Expect(err).ToNot(HaveOccurred()) + _ = gitIgnoreFile.Close() + + Expect(newContent).To(Equal(content)) + }) + }) + + Describe("PushChanges", func() { + JustBeforeEach(func() { + state.WorkspaceId = 42 + state.Branch = branchName + + err := state.Save() + Expect(err).ToNot(HaveOccurred()) + + mockGit.EXPECT().Checkout(branchName, false).Return(nil) + mockGit.EXPECT().AddAll().Return(nil) + }) + Context("when there are changes to push", func() { + It("should push changes to the correct branch", func() { + mockGit.EXPECT().HasChanges(remote, branchName).Return(true, nil) + mockGit.EXPECT().Commit("cs up commit").Return(nil) + mockGit.EXPECT().Push(remote, branchName).Return(nil) + mockGit.EXPECT().PrintStatus().Return(nil) + + err := mgr.PushChanges(false) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("when there are no changes to push", func() { + It("should not attempt to push", func() { + mockGit.EXPECT().HasChanges(remote, branchName).Return(false, nil) + + err := mgr.PushChanges(false) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + + Describe("EnsureWorkspace", func() { + Context("when the workspace exists", func() { + It("should wake up the existing workspace", func() { + state.WorkspaceId = 42 + err := state.Save() + Expect(err).ToNot(HaveOccurred()) + + mockClient.EXPECT().GetWorkspace(42).Return(api.Workspace{ + Id: 42, + Name: wsName, + }, nil) + + mockClient.EXPECT().WakeUpWorkspace(42, token, "ci.yml", state.Timeout).Return(nil) + mockClient.EXPECT().SetEnvVarOnWorkspace(42, mock.Anything).Return(nil) + + err = mgr.EnsureWorkspace() + Expect(err).ToNot(HaveOccurred()) + }) + + }) + + Context("when the workspace does not exist", func() { + It("should create a new workspace", func() { + mockGit.EXPECT().GetRemoteUrl("origin").Return("https://myrepo.git", nil) + + mockClient.EXPECT().ListWorkspacePlans().Return([]api.WorkspacePlan{{Id: 8}}, nil) + mockClient.EXPECT().DeployWorkspace(mock.Anything).Return(&api.Workspace{ + Id: 42, + Name: wsName, + }, nil) + + err := mgr.EnsureWorkspace() + Expect(err).ToNot(HaveOccurred()) + }) + + Context("when the domain type is private", func() { + BeforeEach(func() { + state.DomainType = cs.PrivateDevDomain + }) + It("should create a new workspace with the correct domain type", func() { + mockGit.EXPECT().GetRemoteUrl("origin").Return("https://myrepo.git", nil) + + mockClient.EXPECT().ListWorkspacePlans().Return([]api.WorkspacePlan{{Id: 8}}, nil) + mockClient.EXPECT().DeployWorkspace(mock.MatchedBy(func(args api.DeployWorkspaceArgs) bool { + return *args.Restricted == true + })).Return(&api.Workspace{ + Id: 42, + Name: wsName, + }, nil) + + err := mgr.EnsureWorkspace() + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("when the git repo is private", func() { + BeforeEach(func() { + state.RepoAccess = cs.PrivateRepo + }) + It("should create a new workspace with the correct domain type", func() { + mockGit.EXPECT().GetRemoteUrl("origin").Return("https://myrepo.git", nil) + + mockClient.EXPECT().ListWorkspacePlans().Return([]api.WorkspacePlan{{Id: 8}}, nil) + mockClient.EXPECT().DeployWorkspace(mock.MatchedBy(func(args api.DeployWorkspaceArgs) bool { + return args.IsPrivateRepo == true + })).Return(&api.Workspace{ + Id: 42, + Name: wsName, + }, nil) + + err := mgr.EnsureWorkspace() + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + + }) + }) +}) + +func validateState(fs *util.FileSystem, expected *cs.UpState) { + actual := &cs.UpState{} + err := actual.Load(".cs-up.yaml", fakeTime, fs) + Expect(err).ToNot(HaveOccurred()) + + Expect(actual.WorkspaceId).To(Equal(expected.WorkspaceId)) + Expect(actual.WorkspaceName).To(Equal(expected.WorkspaceName)) + Expect(actual.Profile).To(Equal(expected.Profile)) + Expect(actual.Timeout).To(Equal(expected.Timeout)) + Expect(actual.Branch).To(Equal(expected.Branch)) + Expect(actual.TeamId).To(Equal(expected.TeamId)) + Expect(actual.Plan).To(Equal(expected.Plan)) + Expect(actual.BaseImage).To(Equal(expected.BaseImage)) + Expect(actual.Env).To(Equal(expected.Env)) + Expect(actual.DomainType).To(Equal(expected.DomainType)) + Expect(actual.RepoAccess).To(Equal(expected.RepoAccess)) + Expect(actual.Remote).To(Equal(expected.Remote)) +} diff --git a/pkg/cs/util.go b/pkg/cs/util.go index d766f2a..4144057 100644 --- a/pkg/cs/util.go +++ b/pkg/cs/util.go @@ -23,9 +23,8 @@ type ReplicaStatus struct { Server string `json:"server"` } -func GetPipelineStatus(ws int, stage string) (res []ReplicaStatus, err error) { - - status, err := Get(fmt.Sprintf("workspaces/%d/pipeline/%s", ws, stage)) +func GetPipelineStatus(ws int, stage string, statefile string) (res []ReplicaStatus, err error) { + status, err := Get(fmt.Sprintf("workspaces/%d/pipeline/%s", ws, stage), statefile) if err != nil { err = fmt.Errorf("failed to get pipeline status: %w", err) return @@ -39,13 +38,13 @@ func GetPipelineStatus(ws int, stage string) (res []ReplicaStatus, err error) { return } -func Get(path string) (body []byte, err error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", NewEnv().GetApiUrl(), strings.TrimPrefix(path, "/")), http.NoBody) +func Get(path string, statefile string) (body []byte, err error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", NewEnv(statefile).GetApiUrl(), strings.TrimPrefix(path, "/")), http.NoBody) if err != nil { err = fmt.Errorf("failed to create request: %w", err) return } - err = SetAuthoriziationHeader(req) + err = SetAuthoriziationHeader(req, statefile) if err != nil { err = fmt.Errorf("failed to set header: %w", err) return @@ -61,8 +60,8 @@ func Get(path string) (body []byte, err error) { return } -func SetAuthoriziationHeader(req *http.Request) error { - token, err := NewEnv().GetApiToken() +func SetAuthoriziationHeader(req *http.Request, statefile string) error { + token, err := NewEnv(statefile).GetApiToken() if err != nil { return fmt.Errorf("failed to get API token: %w", err) } diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go index 821d41b..1d5028f 100644 --- a/pkg/exporter/exporter.go +++ b/pkg/exporter/exporter.go @@ -12,7 +12,7 @@ import ( "path/filepath" "github.com/codesphere-cloud/cs-go/pkg/ci" - "github.com/codesphere-cloud/cs-go/pkg/cs" + "github.com/codesphere-cloud/cs-go/pkg/util" templates "github.com/codesphere-cloud/cs-go/tmpl/docker" "github.com/codesphere-cloud/cs-go/tmpl/k8s" ) @@ -25,7 +25,7 @@ type Exporter interface { } type ExporterService struct { - fs *cs.FileSystem + fs *util.FileSystem ymlContent *ci.CiYml outputPath string baseImage string @@ -34,7 +34,7 @@ type ExporterService struct { force bool } -func NewExporterService(fs *cs.FileSystem, outputPath string, baseImage string, envVars []string, repoRoot string, force bool) Exporter { +func NewExporterService(fs *util.FileSystem, outputPath string, baseImage string, envVars []string, repoRoot string, force bool) Exporter { return &ExporterService{ fs: fs, outputPath: outputPath, diff --git a/pkg/exporter/exporter_test.go b/pkg/exporter/exporter_test.go index e0b5bd8..8d90936 100644 --- a/pkg/exporter/exporter_test.go +++ b/pkg/exporter/exporter_test.go @@ -8,8 +8,8 @@ import ( . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" - "github.com/codesphere-cloud/cs-go/pkg/cs" "github.com/codesphere-cloud/cs-go/pkg/exporter" + "github.com/codesphere-cloud/cs-go/pkg/util" ) const ymlContent = ` @@ -32,7 +32,7 @@ run: var _ = Describe("GenerateDockerfile", func() { var ( - memoryFs *cs.FileSystem + memoryFs *util.FileSystem e exporter.Exporter defaultInput string defaultOutput string @@ -43,7 +43,7 @@ var _ = Describe("GenerateDockerfile", func() { defaultInput = "ci.yml" defaultOutput = "./export" defaultBaseImage = "alpine:latest" - memoryFs = cs.NewMemFileSystem() + memoryFs = util.NewMemFileSystem() e = exporter.NewExporterService(memoryFs, defaultOutput, defaultBaseImage, []string{}, "workspace-repo", false) }) diff --git a/pkg/git/git.go b/pkg/git/git.go index 85d8dd0..cc14a67 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -5,28 +5,117 @@ package git import ( "fmt" + "log" "os" + "os/exec" + "strings" - "github.com/codesphere-cloud/cs-go/pkg/cs" + "github.com/codesphere-cloud/cs-go/pkg/util" "github.com/go-git/go-git/v5" //"github.com/go-git/go-git/v5/plumbing" ) type Git interface { - CloneRepository(fs *cs.FileSystem, url string, branch string, path string) (*git.Repository, error) + CloneRepository(fs *util.FileSystem, url string, branch string, path string) (*git.Repository, error) + GetRemoteUrl(remoteName string) (string, error) + Checkout(branch string, createBranch bool) error + AddAll() error + HasChanges(remote string, branch string) (bool, error) + PrintStatus() error + Commit(message string) error + Push(remote string, branch string) error } type GitService struct { - fs *cs.FileSystem + fs *util.FileSystem } -func NewGitService(fs *cs.FileSystem) *GitService { +// Checkout checks out the specified branch, if createBranch is true, it will create the branch +func (g *GitService) Checkout(branch string, createBranch bool) error { + cmd := gitCommand("checkout", branch) + if createBranch { + cmd = gitCommand("checkout", "-b", branch) + } + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to checkout branch: %w", err) + } + return nil +} + +// AddAll stages all changes in the repository using `git add .` +func (g *GitService) AddAll() error { + cmd := gitCommand("add", ".") + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to add files: %w", err) + } + return nil +} + +// HasChanges checks if there are any changes compared to the specified remote and branch without printing the diff +func (g *GitService) HasChanges(remote string, branch string) (bool, error) { + cmd := gitCommand("diff", remote+"/"+branch, "--cached", "--exit-code", "--quiet") + err := cmd.Run() + if err == nil { + return false, nil + } + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == 1 { + return true, nil + } + } + return false, fmt.Errorf("failed to check for changes compared to remote: %w", err) +} + +// PrintStatus prints the output of `git status --short` to stdout +func (g *GitService) PrintStatus() error { + cmd := gitCommand("status", "--short") + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to print git status: %w", err) + } + return nil +} + +// Commit commits the changes with the specified commit message +func (g *GitService) Commit(message string) error { + cmd := gitCommand("commit", "-m", message) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to commit changes: %w", err) + } + return nil +} + +// Push pushes the changes to the specified remote and branch +func (g *GitService) Push(remote string, branch string) error { + cmd := gitCommand("push", remote, "HEAD:"+branch) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to push changes: %w", err) + } + return nil +} + +// GetCurrentBranch returns the name of the current branch +func gitCommand(args ...string) *exec.Cmd { + cmd := exec.Command("git", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Printf("Running git command: git %s\n", strings.Join(args, " ")) + return cmd +} + +// NewGitService creates a new GitService with the provided FileSystem +func NewGitService(fs *util.FileSystem) *GitService { return &GitService{ fs: fs, } } -func (g *GitService) CloneRepository(fs *cs.FileSystem, url string, branch string, path string) (*git.Repository, error) { +// CloneRepository clones the repository from the specified URL and branch to the specified path using go-git library +func (g *GitService) CloneRepository(fs *util.FileSystem, url string, branch string, path string) (*git.Repository, error) { repo, err := git.PlainClone(path, false, &git.CloneOptions{ //repo, err := git.Clone(fs.Storer, fs, &git.CloneOptions{ //ReferenceName: plumbing.NewBranchReferenceName(branch), @@ -39,3 +128,22 @@ func (g *GitService) CloneRepository(fs *cs.FileSystem, url string, branch strin return repo, nil } + +// GetRemoteUrl returns the URL of the specified remote using go-git library +func (g *GitService) GetRemoteUrl(remoteName string) (string, error) { + repo, err := git.PlainOpen(".") + if err != nil { + return "", fmt.Errorf("error opening repository: %w", err) + } + remote, err := repo.Remote(remoteName) + if err != nil { + return "", fmt.Errorf("error getting remote: %w", err) + } + + urls := remote.Config().URLs + if len(urls) == 0 { + return "", fmt.Errorf("no URLs found for remote %s", remoteName) + } + + return urls[0], nil +} diff --git a/pkg/git/mocks.go b/pkg/git/mocks.go index fa2afdb..e3ee73c 100644 --- a/pkg/git/mocks.go +++ b/pkg/git/mocks.go @@ -5,7 +5,7 @@ package git import ( - "github.com/codesphere-cloud/cs-go/pkg/cs" + "github.com/codesphere-cloud/cs-go/pkg/util" "github.com/go-git/go-git/v5" mock "github.com/stretchr/testify/mock" ) @@ -37,8 +37,109 @@ func (_m *MockGit) EXPECT() *MockGit_Expecter { return &MockGit_Expecter{mock: &_m.Mock} } +// AddAll provides a mock function for the type MockGit +func (_mock *MockGit) AddAll() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for AddAll") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockGit_AddAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAll' +type MockGit_AddAll_Call struct { + *mock.Call +} + +// AddAll is a helper method to define mock.On call +func (_e *MockGit_Expecter) AddAll() *MockGit_AddAll_Call { + return &MockGit_AddAll_Call{Call: _e.mock.On("AddAll")} +} + +func (_c *MockGit_AddAll_Call) Run(run func()) *MockGit_AddAll_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockGit_AddAll_Call) Return(err error) *MockGit_AddAll_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockGit_AddAll_Call) RunAndReturn(run func() error) *MockGit_AddAll_Call { + _c.Call.Return(run) + return _c +} + +// Checkout provides a mock function for the type MockGit +func (_mock *MockGit) Checkout(branch string, createBranch bool) error { + ret := _mock.Called(branch, createBranch) + + if len(ret) == 0 { + panic("no return value specified for Checkout") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok { + r0 = returnFunc(branch, createBranch) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockGit_Checkout_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Checkout' +type MockGit_Checkout_Call struct { + *mock.Call +} + +// Checkout is a helper method to define mock.On call +// - branch string +// - createBranch bool +func (_e *MockGit_Expecter) Checkout(branch interface{}, createBranch interface{}) *MockGit_Checkout_Call { + return &MockGit_Checkout_Call{Call: _e.mock.On("Checkout", branch, createBranch)} +} + +func (_c *MockGit_Checkout_Call) Run(run func(branch string, createBranch bool)) *MockGit_Checkout_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockGit_Checkout_Call) Return(err error) *MockGit_Checkout_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockGit_Checkout_Call) RunAndReturn(run func(branch string, createBranch bool) error) *MockGit_Checkout_Call { + _c.Call.Return(run) + return _c +} + // CloneRepository provides a mock function for the type MockGit -func (_mock *MockGit) CloneRepository(fs *cs.FileSystem, url string, branch string, path string) (*git.Repository, error) { +func (_mock *MockGit) CloneRepository(fs *util.FileSystem, url string, branch string, path string) (*git.Repository, error) { ret := _mock.Called(fs, url, branch, path) if len(ret) == 0 { @@ -47,17 +148,17 @@ func (_mock *MockGit) CloneRepository(fs *cs.FileSystem, url string, branch stri var r0 *git.Repository var r1 error - if returnFunc, ok := ret.Get(0).(func(*cs.FileSystem, string, string, string) (*git.Repository, error)); ok { + if returnFunc, ok := ret.Get(0).(func(*util.FileSystem, string, string, string) (*git.Repository, error)); ok { return returnFunc(fs, url, branch, path) } - if returnFunc, ok := ret.Get(0).(func(*cs.FileSystem, string, string, string) *git.Repository); ok { + if returnFunc, ok := ret.Get(0).(func(*util.FileSystem, string, string, string) *git.Repository); ok { r0 = returnFunc(fs, url, branch, path) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*git.Repository) } } - if returnFunc, ok := ret.Get(1).(func(*cs.FileSystem, string, string, string) error); ok { + if returnFunc, ok := ret.Get(1).(func(*util.FileSystem, string, string, string) error); ok { r1 = returnFunc(fs, url, branch, path) } else { r1 = ret.Error(1) @@ -71,7 +172,7 @@ type MockGit_CloneRepository_Call struct { } // CloneRepository is a helper method to define mock.On call -// - fs *cs.FileSystem +// - fs *util.FileSystem // - url string // - branch string // - path string @@ -79,11 +180,11 @@ func (_e *MockGit_Expecter) CloneRepository(fs interface{}, url interface{}, bra return &MockGit_CloneRepository_Call{Call: _e.mock.On("CloneRepository", fs, url, branch, path)} } -func (_c *MockGit_CloneRepository_Call) Run(run func(fs *cs.FileSystem, url string, branch string, path string)) *MockGit_CloneRepository_Call { +func (_c *MockGit_CloneRepository_Call) Run(run func(fs *util.FileSystem, url string, branch string, path string)) *MockGit_CloneRepository_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 *cs.FileSystem + var arg0 *util.FileSystem if args[0] != nil { - arg0 = args[0].(*cs.FileSystem) + arg0 = args[0].(*util.FileSystem) } var arg1 string if args[1] != nil { @@ -112,7 +213,285 @@ func (_c *MockGit_CloneRepository_Call) Return(repository *git.Repository, err e return _c } -func (_c *MockGit_CloneRepository_Call) RunAndReturn(run func(fs *cs.FileSystem, url string, branch string, path string) (*git.Repository, error)) *MockGit_CloneRepository_Call { +func (_c *MockGit_CloneRepository_Call) RunAndReturn(run func(fs *util.FileSystem, url string, branch string, path string) (*git.Repository, error)) *MockGit_CloneRepository_Call { + _c.Call.Return(run) + return _c +} + +// Commit provides a mock function for the type MockGit +func (_mock *MockGit) Commit(message string) error { + ret := _mock.Called(message) + + if len(ret) == 0 { + panic("no return value specified for Commit") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(message) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockGit_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' +type MockGit_Commit_Call struct { + *mock.Call +} + +// Commit is a helper method to define mock.On call +// - message string +func (_e *MockGit_Expecter) Commit(message interface{}) *MockGit_Commit_Call { + return &MockGit_Commit_Call{Call: _e.mock.On("Commit", message)} +} + +func (_c *MockGit_Commit_Call) Run(run func(message string)) *MockGit_Commit_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockGit_Commit_Call) Return(err error) *MockGit_Commit_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockGit_Commit_Call) RunAndReturn(run func(message string) error) *MockGit_Commit_Call { + _c.Call.Return(run) + return _c +} + +// GetRemoteUrl provides a mock function for the type MockGit +func (_mock *MockGit) GetRemoteUrl(remoteName string) (string, error) { + ret := _mock.Called(remoteName) + + if len(ret) == 0 { + panic("no return value specified for GetRemoteUrl") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (string, error)); ok { + return returnFunc(remoteName) + } + if returnFunc, ok := ret.Get(0).(func(string) string); ok { + r0 = returnFunc(remoteName) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(remoteName) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockGit_GetRemoteUrl_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRemoteUrl' +type MockGit_GetRemoteUrl_Call struct { + *mock.Call +} + +// GetRemoteUrl is a helper method to define mock.On call +// - remoteName string +func (_e *MockGit_Expecter) GetRemoteUrl(remoteName interface{}) *MockGit_GetRemoteUrl_Call { + return &MockGit_GetRemoteUrl_Call{Call: _e.mock.On("GetRemoteUrl", remoteName)} +} + +func (_c *MockGit_GetRemoteUrl_Call) Run(run func(remoteName string)) *MockGit_GetRemoteUrl_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockGit_GetRemoteUrl_Call) Return(s string, err error) *MockGit_GetRemoteUrl_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockGit_GetRemoteUrl_Call) RunAndReturn(run func(remoteName string) (string, error)) *MockGit_GetRemoteUrl_Call { + _c.Call.Return(run) + return _c +} + +// HasChanges provides a mock function for the type MockGit +func (_mock *MockGit) HasChanges(remote string, branch string) (bool, error) { + ret := _mock.Called(remote, branch) + + if len(ret) == 0 { + panic("no return value specified for HasChanges") + } + + var r0 bool + var r1 error + if returnFunc, ok := ret.Get(0).(func(string, string) (bool, error)); ok { + return returnFunc(remote, branch) + } + if returnFunc, ok := ret.Get(0).(func(string, string) bool); ok { + r0 = returnFunc(remote, branch) + } else { + r0 = ret.Get(0).(bool) + } + if returnFunc, ok := ret.Get(1).(func(string, string) error); ok { + r1 = returnFunc(remote, branch) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockGit_HasChanges_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasChanges' +type MockGit_HasChanges_Call struct { + *mock.Call +} + +// HasChanges is a helper method to define mock.On call +// - remote string +// - branch string +func (_e *MockGit_Expecter) HasChanges(remote interface{}, branch interface{}) *MockGit_HasChanges_Call { + return &MockGit_HasChanges_Call{Call: _e.mock.On("HasChanges", remote, branch)} +} + +func (_c *MockGit_HasChanges_Call) Run(run func(remote string, branch string)) *MockGit_HasChanges_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockGit_HasChanges_Call) Return(b bool, err error) *MockGit_HasChanges_Call { + _c.Call.Return(b, err) + return _c +} + +func (_c *MockGit_HasChanges_Call) RunAndReturn(run func(remote string, branch string) (bool, error)) *MockGit_HasChanges_Call { + _c.Call.Return(run) + return _c +} + +// PrintStatus provides a mock function for the type MockGit +func (_mock *MockGit) PrintStatus() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for PrintStatus") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockGit_PrintStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PrintStatus' +type MockGit_PrintStatus_Call struct { + *mock.Call +} + +// PrintStatus is a helper method to define mock.On call +func (_e *MockGit_Expecter) PrintStatus() *MockGit_PrintStatus_Call { + return &MockGit_PrintStatus_Call{Call: _e.mock.On("PrintStatus")} +} + +func (_c *MockGit_PrintStatus_Call) Run(run func()) *MockGit_PrintStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockGit_PrintStatus_Call) Return(err error) *MockGit_PrintStatus_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockGit_PrintStatus_Call) RunAndReturn(run func() error) *MockGit_PrintStatus_Call { + _c.Call.Return(run) + return _c +} + +// Push provides a mock function for the type MockGit +func (_mock *MockGit) Push(remote string, branch string) error { + ret := _mock.Called(remote, branch) + + if len(ret) == 0 { + panic("no return value specified for Push") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { + r0 = returnFunc(remote, branch) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockGit_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push' +type MockGit_Push_Call struct { + *mock.Call +} + +// Push is a helper method to define mock.On call +// - remote string +// - branch string +func (_e *MockGit_Expecter) Push(remote interface{}, branch interface{}) *MockGit_Push_Call { + return &MockGit_Push_Call{Call: _e.mock.On("Push", remote, branch)} +} + +func (_c *MockGit_Push_Call) Run(run func(remote string, branch string)) *MockGit_Push_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockGit_Push_Call) Return(err error) *MockGit_Push_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockGit_Push_Call) RunAndReturn(run func(remote string, branch string) error) *MockGit_Push_Call { _c.Call.Return(run) return _c } diff --git a/pkg/pipeline/pipeline.go b/pkg/pipeline/pipeline.go new file mode 100644 index 0000000..f99fb6a --- /dev/null +++ b/pkg/pipeline/pipeline.go @@ -0,0 +1,147 @@ +package pipeline + +import ( + "fmt" + "log" + "slices" + "time" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/codesphere-cloud/cs-go/pkg/io" +) + +const IdeServer string = "codesphere-ide" + +type PipelineRunner struct { + Client api.Client + Profile string + Time api.Time + Timeout time.Duration + VerboseOutput bool +} + +func NewPipelineRunner(client api.Client, profile string, timeout time.Duration, verboseOutput bool) *PipelineRunner { + return &PipelineRunner{ + Client: client, + Profile: profile, + Time: &api.RealTime{}, + Timeout: timeout, + VerboseOutput: verboseOutput, + } +} + +func NewPipelineRunnerWidthCustomDeps(client api.Client, profile string, time api.Time, timeout time.Duration, verboseOutput bool) *PipelineRunner { + return &PipelineRunner{ + Client: client, + Profile: profile, + Time: time, + Timeout: timeout, + VerboseOutput: verboseOutput, + } +} + +func (pr *PipelineRunner) StartPipelineStages(wsId int, stages []string) error { + for _, stage := range stages { + if !isValidStage(stage) { + return fmt.Errorf("invalid pipeline stage: %s", stage) + } + } + for _, stage := range stages { + err := pr.StartStage(wsId, pr.Profile, stage, pr.Time, pr.Timeout, pr.VerboseOutput) + if err != nil { + return err + } + } + return nil +} + +func isValidStage(stage string) bool { + return slices.Contains([]string{"prepare", "test", "run"}, stage) +} + +func (pr *PipelineRunner) StartStage(wsId int, profile string, stage string, timeType api.Time, timeout time.Duration, verboseOutput bool) error { + fmt.Printf("starting %s stage on workspace %d...", stage, wsId) + + err := pr.Client.StartPipelineStage(wsId, profile, stage) + if err != nil { + fmt.Println() + return fmt.Errorf("failed to start pipeline stage %s: %w", stage, err) + } + + err = pr.waitForPipelineStage(wsId, stage, timeType, timeout, verboseOutput) + if err != nil { + return fmt.Errorf("failed waiting for stage %s to finish: %w", stage, err) + + } + return nil +} + +func (pr *PipelineRunner) waitForPipelineStage(wsId int, stage string, timeType api.Time, timeout time.Duration, verboseOutput bool) error { + delay := 5 * time.Second + + maxWaitTime := timeType.Now().Add(timeout) + for { + status, err := pr.Client.GetPipelineState(wsId, stage) + if err != nil { + log.Printf("\nError getting pipeline status: %s, trying again...", err.Error()) + timeType.Sleep(delay) + continue + } + + if allFinished(status, verboseOutput) { + log.Println("(finished)") + break + } + + if allRunning(status) && stage == "run" { + log.Println("(running)") + break + } + + err = shouldAbort(status) + if err != nil { + log.Println("(failed)") + return fmt.Errorf("stage %s failed: %w", stage, err) + } + + fmt.Print(".") + if timeType.Now().After(maxWaitTime) { + log.Println() + return fmt.Errorf("timed out waiting for pipeline stage %s to be complete", stage) + } + timeType.Sleep(delay) + } + return nil +} + +func allRunning(status []api.PipelineStatus) bool { + for _, s := range status { + // Run stage is only running customer servers, ignore IDE server + if s.Server != IdeServer && s.State != "running" { + return false + } + } + return true +} + +func allFinished(status []api.PipelineStatus, verboseOutput bool) bool { + for _, s := range status { + io.Verbosef(verboseOutput, "Server: %s, State: %s, Replica: %s\n", s.Server, s.State, s.Replica) + } + for _, s := range status { + // Prepare and Test stage is only running in the IDE server, ignore customer servers + if s.Server == IdeServer && s.State != "success" { + return false + } + } + return true +} + +func shouldAbort(status []api.PipelineStatus) error { + for _, s := range status { + if slices.Contains([]string{"failure", "aborted"}, s.State) { + return fmt.Errorf("server %s, replica %s reached unexpected state %s", s.Server, s.Replica, s.State) + } + } + return nil +} diff --git a/cli/cmd/start_pipeline_test.go b/pkg/pipeline/start_pipeline_test.go similarity index 90% rename from cli/cmd/start_pipeline_test.go rename to pkg/pipeline/start_pipeline_test.go index 9a1b0c8..ca3f020 100644 --- a/cli/cmd/start_pipeline_test.go +++ b/pkg/pipeline/start_pipeline_test.go @@ -1,7 +1,7 @@ // Copyright (c) Codesphere Inc. // SPDX-License-Identifier: Apache-2.0 -package cmd_test +package pipeline_test import ( "time" @@ -12,41 +12,32 @@ import ( "github.com/codesphere-cloud/cs-go/api" "github.com/codesphere-cloud/cs-go/cli/cmd" + "github.com/codesphere-cloud/cs-go/pkg/pipeline" ) var _ = Describe("StartPipeline", func() { var ( - mockClient *cmd.MockClient mockTime *api.MockTime - c *cmd.StartPipelineCmd + mockClient *cmd.MockClient wsId int timeout time.Duration stages []string profile string verbose bool + pr *pipeline.PipelineRunner ) BeforeEach(func() { - mockClient = cmd.NewMockClient(GinkgoT()) mockTime = api.NewMockTime(GinkgoT()) wsId = 21 profile = "" timeout = 30 * time.Second verbose = false + mockClient = cmd.NewMockClient(GinkgoT()) }) JustBeforeEach(func() { - c = &cmd.StartPipelineCmd{ - Opts: cmd.StartPipelineOpts{ - GlobalOptions: &cmd.GlobalOptions{ - WorkspaceId: wsId, - Verbose: verbose, - }, - Profile: &profile, - Timeout: &timeout, - }, - Time: mockTime, - } + pr = pipeline.NewPipelineRunnerWidthCustomDeps(mockClient, profile, mockTime, timeout, verbose) }) Context("invalid pipeline stage specified", func() { @@ -55,7 +46,7 @@ var _ = Describe("StartPipeline", func() { }) It("fails before executing any stage", func() { - err := c.StartPipelineStages(mockClient, wsId, stages) + err := pr.StartPipelineStages(wsId, stages) Expect(err).To(MatchError("invalid pipeline stage: " + stages[0])) }) }) @@ -104,13 +95,13 @@ var _ = Describe("StartPipeline", func() { profile = "prod" }) It("starts all 3 stages sequentially", func() { - err := c.StartPipelineStages(mockClient, wsId, stages) + err := pr.StartPipelineStages(wsId, stages) Expect(err).NotTo(HaveOccurred()) }) }) It("starts all 3 stages sequentially", func() { - err := c.StartPipelineStages(mockClient, wsId, stages) + err := pr.StartPipelineStages(wsId, stages) Expect(err).NotTo(HaveOccurred()) }) }) @@ -129,7 +120,7 @@ var _ = Describe("StartPipeline", func() { mockClient.EXPECT().GetPipelineState(wsId, stages[2]).Return(reportedStatusWaiting, nil).Times(2).NotBefore(runStartCall) mockClient.EXPECT().GetPipelineState(wsId, stages[2]).Return(reportedStatusRunning, nil).NotBefore(runStartCall) - err := c.StartPipelineStages(mockClient, wsId, stages) + err := pr.StartPipelineStages(wsId, stages) Expect(err).NotTo(HaveOccurred()) }) }) @@ -144,7 +135,7 @@ var _ = Describe("StartPipeline", func() { //this should result in a timeout mockClient.EXPECT().GetPipelineState(wsId, stages[1]).Return(reportedStatusRunning, nil).Times(8).NotBefore(testStartCall) - err := c.StartPipelineStages(mockClient, wsId, stages) + err := pr.StartPipelineStages(wsId, stages) Expect(err).To(MatchError("failed waiting for stage test to finish: timed out waiting for pipeline stage test to be complete")) }) }) @@ -154,7 +145,7 @@ var _ = Describe("StartPipeline", func() { prepareStartCall := mockClient.EXPECT().StartPipelineStage(wsId, profile, stages[0]).Return(nil).Call mockClient.EXPECT().GetPipelineState(wsId, stages[0]).Return(reportedStatusFailure, nil).NotBefore(prepareStartCall) - err := c.StartPipelineStages(mockClient, wsId, stages) + err := pr.StartPipelineStages(wsId, stages) Expect(err).To(MatchError("failed waiting for stage prepare to finish: stage prepare failed: server A, replica 0 reached unexpected state failure")) }) }) diff --git a/pkg/testutil/time.go b/pkg/testutil/time.go new file mode 100644 index 0000000..e30dd96 --- /dev/null +++ b/pkg/testutil/time.go @@ -0,0 +1,29 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package testutil + +import ( + "time" + + "github.com/codesphere-cloud/cs-go/api" + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/mock" +) + +func MockTime() *api.MockTime { + currentTime := time.Unix(1746190963, 0) + return MockTimeAt(currentTime) +} + +func MockTimeAt(t time.Time) *api.MockTime { + currentTime := t + m := api.NewMockTime(ginkgo.GinkgoT()) + m.EXPECT().Now().RunAndReturn(func() time.Time { + return currentTime + }).Maybe() + m.EXPECT().Sleep(mock.Anything).Run(func(delay time.Duration) { + currentTime = currentTime.Add(delay) + }).Maybe() + return m +} diff --git a/pkg/cs/file_system.go b/pkg/util/file_system.go similarity index 84% rename from pkg/cs/file_system.go rename to pkg/util/file_system.go index 9ee3021..6e08e6c 100644 --- a/pkg/cs/file_system.go +++ b/pkg/util/file_system.go @@ -1,10 +1,11 @@ // Copyright (c) Codesphere Inc. // SPDX-License-Identifier: Apache-2.0 -package cs +package util import ( "fmt" + "io" "os" "path/filepath" @@ -81,6 +82,22 @@ func (f *FileSystem) CreateFile(filename string) (billy.File, error) { return file, nil } +// ReadFile reads the contents of the specified file and returns it as a byte slice. +func (f *FileSystem) ReadFile(filename string) ([]byte, error) { + file, err := f.Open(filename) + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + defer func() { _ = file.Close() }() + + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + return data, nil +} + // WriteFile creates a file at the specified path and writes data to it. // If the directory does not exist, it will be created. // If the file in the directory already exists, it returns an error. From a24bf480afc6dbaf68bea75c2b1932e1685faef6 Mon Sep 17 00:00:00 2001 From: NautiluX <2600004+NautiluX@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:18:39 +0000 Subject: [PATCH 2/2] chore(docs): Auto-update docs and licenses Signed-off-by: NautiluX <2600004+NautiluX@users.noreply.github.com> --- NOTICE | 20 ++++++------- api/wakeup.go | 3 ++ cli/cmd/ps.go | 3 ++ cli/cmd/up.go | 3 ++ docs/README.md | 13 +++++---- docs/cs.md | 13 +++++---- docs/cs_create.md | 9 +++--- docs/cs_create_workspace.md | 9 +++--- docs/cs_curl.md | 9 +++--- docs/cs_delete.md | 9 +++--- docs/cs_delete_workspace.md | 9 +++--- docs/cs_exec.md | 9 +++--- docs/cs_generate.md | 9 +++--- docs/cs_generate_docker.md | 19 +++++++------ docs/cs_generate_images.md | 19 +++++++------ docs/cs_generate_kubernetes.md | 19 +++++++------ docs/cs_git.md | 9 +++--- docs/cs_git_pull.md | 9 +++--- docs/cs_licenses.md | 9 +++--- docs/cs_list.md | 9 +++--- docs/cs_list_baseimages.md | 11 ++++---- docs/cs_list_plans.md | 11 ++++---- docs/cs_list_teams.md | 11 ++++---- docs/cs_list_workspaces.md | 11 ++++---- docs/cs_log.md | 9 +++--- docs/cs_monitor.md | 9 +++--- docs/cs_open.md | 9 +++--- docs/cs_open_workspace.md | 9 +++--- docs/cs_ps.md | 32 +++++++++++++++++++++ docs/cs_scale.md | 9 +++--- docs/cs_scale_workspace.md | 9 +++--- docs/cs_set-env.md | 9 +++--- docs/cs_start.md | 9 +++--- docs/cs_start_pipeline.md | 9 +++--- docs/cs_sync.md | 9 +++--- docs/cs_sync_landscape.md | 9 +++--- docs/cs_up.md | 51 ++++++++++++++++++++++++++++++++++ docs/cs_update.md | 9 +++--- docs/cs_version.md | 9 +++--- docs/cs_wake-up.md | 14 +++++----- pkg/cs/.cs-up.yaml | 3 ++ pkg/cs/state.go | 3 ++ pkg/cs/up.go | 3 ++ pkg/pipeline/pipeline.go | 3 ++ pkg/tmpl/NOTICE | 20 ++++++------- 45 files changed, 321 insertions(+), 180 deletions(-) create mode 100644 docs/cs_ps.md create mode 100644 docs/cs_up.md diff --git a/NOTICE b/NOTICE index c146e1e..95f30c8 100644 --- a/NOTICE +++ b/NOTICE @@ -5,9 +5,9 @@ This project includes code licensed under the following terms: ---------- Module: code.gitea.io/sdk/gitea -Version: v0.23.2 +Version: v0.24.1 License: MIT -License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.23.2/gitea/LICENSE +License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.24.1/gitea/LICENSE ---------- Module: dario.cat/mergo @@ -17,9 +17,9 @@ License URL: https://github.com/imdario/mergo/blob/v1.0.2/LICENSE ---------- Module: github.com/42wim/httpsig -Version: v1.2.3 +Version: v1.2.4 License: BSD-3-Clause -License URL: https://github.com/42wim/httpsig/blob/v1.2.3/LICENSE +License URL: https://github.com/42wim/httpsig/blob/v1.2.4/LICENSE ---------- Module: github.com/Masterminds/semver/v3 @@ -29,9 +29,9 @@ License URL: https://github.com/Masterminds/semver/blob/v3.4.0/LICENSE.txt ---------- Module: github.com/ProtonMail/go-crypto -Version: v1.3.0 +Version: v1.4.0 License: BSD-3-Clause -License URL: https://github.com/ProtonMail/go-crypto/blob/v1.3.0/LICENSE +License URL: https://github.com/ProtonMail/go-crypto/blob/v1.4.0/LICENSE ---------- Module: github.com/beorn7/perks/quantile @@ -125,9 +125,9 @@ License URL: https://github.com/go-git/go-billy/blob/v5.8.0/LICENSE ---------- Module: github.com/go-git/go-git/v5 -Version: v5.17.0 +Version: v5.17.1 License: Apache-2.0 -License URL: https://github.com/go-git/go-git/blob/v5.17.0/LICENSE +License URL: https://github.com/go-git/go-git/blob/v5.17.1/LICENSE ---------- Module: github.com/go-logr/logr @@ -395,9 +395,9 @@ License URL: https://cs.opensource.google/go/x/net/+/v0.52.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.35.0 +Version: v0.36.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.36.0:LICENSE ---------- Module: golang.org/x/sync/errgroup diff --git a/api/wakeup.go b/api/wakeup.go index 1bbb436..0ea63e6 100644 --- a/api/wakeup.go +++ b/api/wakeup.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package api import ( diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go index 3e5ccd5..1ba4ed4 100644 --- a/cli/cmd/ps.go +++ b/cli/cmd/ps.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package cmd import ( diff --git a/cli/cmd/up.go b/cli/cmd/up.go index f5dec18..b99a9f8 100644 --- a/cli/cmd/up.go +++ b/cli/cmd/up.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package cmd import ( diff --git a/docs/README.md b/docs/README.md index 0b42bd6..61a8cd5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,11 +9,12 @@ Manage and debug resources deployed in Codesphere via command line. ### Options ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -h, --help help for cs - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + -h, --help help for cs + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO @@ -29,10 +30,12 @@ Manage and debug resources deployed in Codesphere via command line. * [cs log](cs_log.md) - Retrieve run logs from services * [cs monitor](cs_monitor.md) - Monitor a command and report health information * [cs open](cs_open.md) - Open the Codesphere IDE +* [cs ps](cs_ps.md) - List services of a workspace * [cs scale](cs_scale.md) - Scale Codesphere resources * [cs set-env](cs_set-env.md) - Set environment variables * [cs start](cs_start.md) - Start workspace pipeline * [cs sync](cs_sync.md) - Sync Codesphere resources +* [cs up](cs_up.md) - Deploy your local code to Codesphere * [cs update](cs_update.md) - Update Codesphere CLI * [cs version](cs_version.md) - Print version * [cs wake-up](cs_wake-up.md) - Wake up an on-demand workspace diff --git a/docs/cs.md b/docs/cs.md index 0b42bd6..61a8cd5 100644 --- a/docs/cs.md +++ b/docs/cs.md @@ -9,11 +9,12 @@ Manage and debug resources deployed in Codesphere via command line. ### Options ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -h, --help help for cs - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + -h, --help help for cs + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO @@ -29,10 +30,12 @@ Manage and debug resources deployed in Codesphere via command line. * [cs log](cs_log.md) - Retrieve run logs from services * [cs monitor](cs_monitor.md) - Monitor a command and report health information * [cs open](cs_open.md) - Open the Codesphere IDE +* [cs ps](cs_ps.md) - List services of a workspace * [cs scale](cs_scale.md) - Scale Codesphere resources * [cs set-env](cs_set-env.md) - Set environment variables * [cs start](cs_start.md) - Start workspace pipeline * [cs sync](cs_sync.md) - Sync Codesphere resources +* [cs up](cs_up.md) - Deploy your local code to Codesphere * [cs update](cs_update.md) - Update Codesphere CLI * [cs version](cs_version.md) - Print version * [cs wake-up](cs_wake-up.md) - Wake up an on-demand workspace diff --git a/docs/cs_create.md b/docs/cs_create.md index 38bf359..25ddd78 100644 --- a/docs/cs_create.md +++ b/docs/cs_create.md @@ -15,10 +15,11 @@ Create codesphere resources like workspaces. ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_create_workspace.md b/docs/cs_create_workspace.md index d256526..64855ad 100644 --- a/docs/cs_create_workspace.md +++ b/docs/cs_create_workspace.md @@ -63,10 +63,11 @@ $ cs create workspace my-workspace -r https://github.com/my-org/my-private-proje ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_curl.md b/docs/cs_curl.md index 73bd022..ab85b8b 100644 --- a/docs/cs_curl.md +++ b/docs/cs_curl.md @@ -45,10 +45,11 @@ $ cs curl / -- -I ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_delete.md b/docs/cs_delete.md index f977093..4fda48a 100644 --- a/docs/cs_delete.md +++ b/docs/cs_delete.md @@ -15,10 +15,11 @@ Delete Codesphere resources, e.g. workspaces. ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_delete_workspace.md b/docs/cs_delete_workspace.md index 4f3deee..d4a8e93 100644 --- a/docs/cs_delete_workspace.md +++ b/docs/cs_delete_workspace.md @@ -22,10 +22,11 @@ cs delete workspace [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_exec.md b/docs/cs_exec.md index 0505840..41c1fc4 100644 --- a/docs/cs_exec.md +++ b/docs/cs_exec.md @@ -38,10 +38,11 @@ $ cs exec -e FOO=bar -- 'echo $FOO' ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_generate.md b/docs/cs_generate.md index c0528bd..70562b1 100644 --- a/docs/cs_generate.md +++ b/docs/cs_generate.md @@ -22,10 +22,11 @@ on your local machine to run the artifact generation. ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_generate_docker.md b/docs/cs_generate_docker.md index 7c0373d..cfbe032 100644 --- a/docs/cs_generate_docker.md +++ b/docs/cs_generate_docker.md @@ -47,15 +47,16 @@ $ cs generate docker -w 1234 -i ci.prod.yml ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - --branch string Branch of the repository to clone if the input file is not found (default "main") - -f, --force Overwrite any files if existing - -i, --input string CI profile to use as input for generation, relative to repository root (default "ci.yml") - -o, --output string Output path of the folder including generated artifacts, relative to repository root (default "export") - --reporoot string root directory of the workspace repository to export. Will be used to clone the repository if it doesn't exist. (default "./workspace-repo") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --branch string Branch of the repository to clone if the input file is not found (default "main") + -f, --force Overwrite any files if existing + -i, --input string CI profile to use as input for generation, relative to repository root (default "ci.yml") + -o, --output string Output path of the folder including generated artifacts, relative to repository root (default "export") + --reporoot string root directory of the workspace repository to export. Will be used to clone the repository if it doesn't exist. (default "./workspace-repo") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_generate_images.md b/docs/cs_generate_images.md index 2897bac..3b188c3 100644 --- a/docs/cs_generate_images.md +++ b/docs/cs_generate_images.md @@ -34,15 +34,16 @@ $ cs generate images -r yourRegistry -p customImagePrefix ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - --branch string Branch of the repository to clone if the input file is not found (default "main") - -f, --force Overwrite any files if existing - -i, --input string CI profile to use as input for generation, relative to repository root (default "ci.yml") - -o, --output string Output path of the folder including generated artifacts, relative to repository root (default "export") - --reporoot string root directory of the workspace repository to export. Will be used to clone the repository if it doesn't exist. (default "./workspace-repo") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --branch string Branch of the repository to clone if the input file is not found (default "main") + -f, --force Overwrite any files if existing + -i, --input string CI profile to use as input for generation, relative to repository root (default "ci.yml") + -o, --output string Output path of the folder including generated artifacts, relative to repository root (default "export") + --reporoot string root directory of the workspace repository to export. Will be used to clone the repository if it doesn't exist. (default "./workspace-repo") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_generate_kubernetes.md b/docs/cs_generate_kubernetes.md index b09337d..2958122 100644 --- a/docs/cs_generate_kubernetes.md +++ b/docs/cs_generate_kubernetes.md @@ -53,15 +53,16 @@ $ cs generate kubernetes -w 1234 -i ci.prod.yml ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - --branch string Branch of the repository to clone if the input file is not found (default "main") - -f, --force Overwrite any files if existing - -i, --input string CI profile to use as input for generation, relative to repository root (default "ci.yml") - -o, --output string Output path of the folder including generated artifacts, relative to repository root (default "export") - --reporoot string root directory of the workspace repository to export. Will be used to clone the repository if it doesn't exist. (default "./workspace-repo") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --branch string Branch of the repository to clone if the input file is not found (default "main") + -f, --force Overwrite any files if existing + -i, --input string CI profile to use as input for generation, relative to repository root (default "ci.yml") + -o, --output string Output path of the folder including generated artifacts, relative to repository root (default "export") + --reporoot string root directory of the workspace repository to export. Will be used to clone the repository if it doesn't exist. (default "./workspace-repo") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_git.md b/docs/cs_git.md index 5723211..61ceed6 100644 --- a/docs/cs_git.md +++ b/docs/cs_git.md @@ -18,10 +18,11 @@ like pulling or switching to a specific branch. ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_git_pull.md b/docs/cs_git_pull.md index e72c903..2001efc 100644 --- a/docs/cs_git_pull.md +++ b/docs/cs_git_pull.md @@ -33,10 +33,11 @@ $ cs git pull --remote origin --branch staging ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_licenses.md b/docs/cs_licenses.md index e08a52c..0533f97 100644 --- a/docs/cs_licenses.md +++ b/docs/cs_licenses.md @@ -19,10 +19,11 @@ cs licenses [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_list.md b/docs/cs_list.md index 4152da9..0ca0f25 100644 --- a/docs/cs_list.md +++ b/docs/cs_list.md @@ -23,10 +23,11 @@ $ cs list workspaces ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_list_baseimages.md b/docs/cs_list_baseimages.md index 481c192..d464866 100644 --- a/docs/cs_list_baseimages.md +++ b/docs/cs_list_baseimages.md @@ -26,11 +26,12 @@ $ cs list baseimages ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -o, --output string Output format (table, json, yaml) (default "table") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + -o, --output string Output format (table, json, yaml) (default "table") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_list_plans.md b/docs/cs_list_plans.md index ae41791..8e47fb8 100644 --- a/docs/cs_list_plans.md +++ b/docs/cs_list_plans.md @@ -21,11 +21,12 @@ cs list plans [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -o, --output string Output format (table, json, yaml) (default "table") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + -o, --output string Output format (table, json, yaml) (default "table") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_list_teams.md b/docs/cs_list_teams.md index 22a716f..570d8e4 100644 --- a/docs/cs_list_teams.md +++ b/docs/cs_list_teams.md @@ -26,11 +26,12 @@ $ cs list teams ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -o, --output string Output format (table, json, yaml) (default "table") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + -o, --output string Output format (table, json, yaml) (default "table") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_list_workspaces.md b/docs/cs_list_workspaces.md index 27ac0e6..3c52095 100644 --- a/docs/cs_list_workspaces.md +++ b/docs/cs_list_workspaces.md @@ -26,11 +26,12 @@ $ cs list workspaces -t ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -o, --output string Output format (table, json, yaml) (default "table") - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + -o, --output string Output format (table, json, yaml) (default "table") + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_log.md b/docs/cs_log.md index 723a0f9..0121ccd 100644 --- a/docs/cs_log.md +++ b/docs/cs_log.md @@ -42,10 +42,11 @@ $ cs log -w 637128 -r workspace-213d7a8c-48b4-42e2-8f70-c905ab04abb5-58d657cdc5- ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_monitor.md b/docs/cs_monitor.md index 7eb96e1..eba8647 100644 --- a/docs/cs_monitor.md +++ b/docs/cs_monitor.md @@ -53,10 +53,11 @@ $ cs monitor --forward --ca-cert-file ca.crt -- ./my-app --healthcheck https://l ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_open.md b/docs/cs_open.md index 5eb7554..3aec124 100644 --- a/docs/cs_open.md +++ b/docs/cs_open.md @@ -19,10 +19,11 @@ cs open [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_open_workspace.md b/docs/cs_open_workspace.md index d870d33..11cdd3c 100644 --- a/docs/cs_open_workspace.md +++ b/docs/cs_open_workspace.md @@ -29,10 +29,11 @@ $ cs open workspace ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_ps.md b/docs/cs_ps.md new file mode 100644 index 0000000..691fb61 --- /dev/null +++ b/docs/cs_ps.md @@ -0,0 +1,32 @@ +## cs ps + +List services of a workspace + +### Synopsis + +Lists all services of a workspace with their current state. + +``` +cs ps [flags] +``` + +### Options + +``` + -h, --help help for ps +``` + +### Options inherited from parent commands + +``` + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) +``` + +### SEE ALSO + +* [cs](cs.md) - The Codesphere CLI + diff --git a/docs/cs_scale.md b/docs/cs_scale.md index 115570e..94bc0b7 100644 --- a/docs/cs_scale.md +++ b/docs/cs_scale.md @@ -15,10 +15,11 @@ Scale Codesphere resources, like landscape services of a workspace. ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_scale_workspace.md b/docs/cs_scale_workspace.md index 6e464ed..c0390fe 100644 --- a/docs/cs_scale_workspace.md +++ b/docs/cs_scale_workspace.md @@ -33,10 +33,11 @@ $ cs scale workspace --service api=0 ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_set-env.md b/docs/cs_set-env.md index f8ee408..8bed55d 100644 --- a/docs/cs_set-env.md +++ b/docs/cs_set-env.md @@ -30,10 +30,11 @@ $ cs set-env --workspace --env-var foo=bar --env-var hello=world ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_start.md b/docs/cs_start.md index e1c88e3..da3ce51 100644 --- a/docs/cs_start.md +++ b/docs/cs_start.md @@ -15,10 +15,11 @@ Start pipeline of a workspace using the pipeline subcommand ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_start_pipeline.md b/docs/cs_start_pipeline.md index 454f620..c57b6d6 100644 --- a/docs/cs_start_pipeline.md +++ b/docs/cs_start_pipeline.md @@ -51,10 +51,11 @@ $ cs start pipeline -t 5m prepare ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_sync.md b/docs/cs_sync.md index 87745fb..6c9a87e 100644 --- a/docs/cs_sync.md +++ b/docs/cs_sync.md @@ -15,10 +15,11 @@ Synchronize Codesphere resources, like infrastructure required to run services. ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_sync_landscape.md b/docs/cs_sync_landscape.md index 6875dd0..4e16809 100644 --- a/docs/cs_sync_landscape.md +++ b/docs/cs_sync_landscape.md @@ -20,10 +20,11 @@ cs sync landscape [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_up.md b/docs/cs_up.md new file mode 100644 index 0000000..b74d89e --- /dev/null +++ b/docs/cs_up.md @@ -0,0 +1,51 @@ +## cs up + +Deploy your local code to Codesphere + +### Synopsis + +Deploys your local code to a new or existing Codepshere workspace. + +Prerequisite: Your code needs to be located in a git repository where you can create and push WIP branches. +When running cs up, the cs CLI will do the following: + +* Push local changes to a branch (customizable with -b), if none is specified, cs go creates a WIP branch (will be stored and reused in .cs-up.yaml) +* Create workspace if it doesn't exist yet (state in .cs-up.yaml) +* Start the deployment in Codesphere, customize which profile to use with -p flag (defaults to 'ci.yml') +* Print the dev domain of the workspace to the console once the deployment is successful + +``` +cs up [flags] +``` + +### Options + +``` + --base-image string Base image to use for the workspace, if not set, the default base image will be used + -b, --branch string Branch to push to, if not set, a WIP branch will be created and reused for subsequent runs + -e, --env stringArray Environment variables to set in the format KEY=VALUE, can be specified multiple times for multiple variables + -h, --help help for up + --plan int Plan ID to use for the workspace, if not set, the first available plan will be used (default -1) + --private-repo string Whether the git repository is public or private (requires authentication), defaults to 'public' + -p, --profile string CI profile to use (e.g. 'ci.dev.yml' for a dev profile, you may have defined in 'ci.dev.yml'), defaults to the ci.yml profile + --public-dev-domain string Whether to create a public or private dev domain for the workspace (only applies to new workspaces), defaults to 'public' + --remote string Git remote to use for pushing the code, defaults to 'origin' (default "origin") + --timeout duration Timeout for the deployment process, e.g. 10m, 1h, defaults to 1m + -v, --verbose Enable verbose output + --workspace-name string Name of the workspace to create, if not set, a random name will be generated + -y, --yes Skip confirmation prompt for pushing changes to the git repository +``` + +### Options inherited from parent commands + +``` + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) +``` + +### SEE ALSO + +* [cs](cs.md) - The Codesphere CLI + diff --git a/docs/cs_update.md b/docs/cs_update.md index 73ce794..95be8ee 100644 --- a/docs/cs_update.md +++ b/docs/cs_update.md @@ -19,10 +19,11 @@ cs update [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_version.md b/docs/cs_version.md index c54be56..a4220eb 100644 --- a/docs/cs_version.md +++ b/docs/cs_version.md @@ -19,10 +19,11 @@ cs version [flags] ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/docs/cs_wake-up.md b/docs/cs_wake-up.md index 05d0ea0..6bc7108 100644 --- a/docs/cs_wake-up.md +++ b/docs/cs_wake-up.md @@ -23,10 +23,10 @@ $ cs wake-up $ cs wake-up -w 1234 --timeout 60s # wake up workspace and deploy landscape from CI profile -$ cs wake-up -w 1234 --sync-landscape +$ cs wake-up -w 1234 # wake up workspace and deploy landscape with prod profile -$ cs wake-up -w 1234 --sync-landscape --profile prod +$ cs wake-up -w 1234 --profile prod ``` ### Options @@ -34,17 +34,17 @@ $ cs wake-up -w 1234 --sync-landscape --profile prod ``` -h, --help help for wake-up -p, --profile string CI profile to use for landscape deploy (e.g. 'prod' for ci.prod.yml) - --sync-landscape Deploy landscape from CI profile after waking up --timeout duration Timeout for waking up the workspace (default 2m0s) ``` ### Options inherited from parent commands ``` - -a, --api string URL of Codesphere API (can also be CS_API) - -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) - -v, --verbose Verbose output - -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) + -a, --api string URL of Codesphere API (can also be CS_API) + --state-file string Path to the state file, defaults to .cs-up.yaml (default ".cs-up.yaml") + -t, --team int Team ID (relevant for some commands, can also be CS_TEAM_ID) (default -1) + -v, --verbose Verbose output + -w, --workspace int Workspace ID (relevant for some commands, can also be CS_WORKSPACE_ID) (default -1) ``` ### SEE ALSO diff --git a/pkg/cs/.cs-up.yaml b/pkg/cs/.cs-up.yaml index cd03aaf..92a7eab 100644 --- a/pkg/cs/.cs-up.yaml +++ b/pkg/cs/.cs-up.yaml @@ -1,3 +1,6 @@ +# Copyright (c) Codesphere Inc. +# SPDX-License-Identifier: Apache-2.0 + profile: "" timeout: 0s branch: "" diff --git a/pkg/cs/state.go b/pkg/cs/state.go index bd3b18a..ca2d675 100644 --- a/pkg/cs/state.go +++ b/pkg/cs/state.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package cs import ( diff --git a/pkg/cs/up.go b/pkg/cs/up.go index 80f9dab..2660d89 100644 --- a/pkg/cs/up.go +++ b/pkg/cs/up.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package cs import ( diff --git a/pkg/pipeline/pipeline.go b/pkg/pipeline/pipeline.go index f99fb6a..3312c0c 100644 --- a/pkg/pipeline/pipeline.go +++ b/pkg/pipeline/pipeline.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package pipeline import ( diff --git a/pkg/tmpl/NOTICE b/pkg/tmpl/NOTICE index c146e1e..95f30c8 100644 --- a/pkg/tmpl/NOTICE +++ b/pkg/tmpl/NOTICE @@ -5,9 +5,9 @@ This project includes code licensed under the following terms: ---------- Module: code.gitea.io/sdk/gitea -Version: v0.23.2 +Version: v0.24.1 License: MIT -License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.23.2/gitea/LICENSE +License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.24.1/gitea/LICENSE ---------- Module: dario.cat/mergo @@ -17,9 +17,9 @@ License URL: https://github.com/imdario/mergo/blob/v1.0.2/LICENSE ---------- Module: github.com/42wim/httpsig -Version: v1.2.3 +Version: v1.2.4 License: BSD-3-Clause -License URL: https://github.com/42wim/httpsig/blob/v1.2.3/LICENSE +License URL: https://github.com/42wim/httpsig/blob/v1.2.4/LICENSE ---------- Module: github.com/Masterminds/semver/v3 @@ -29,9 +29,9 @@ License URL: https://github.com/Masterminds/semver/blob/v3.4.0/LICENSE.txt ---------- Module: github.com/ProtonMail/go-crypto -Version: v1.3.0 +Version: v1.4.0 License: BSD-3-Clause -License URL: https://github.com/ProtonMail/go-crypto/blob/v1.3.0/LICENSE +License URL: https://github.com/ProtonMail/go-crypto/blob/v1.4.0/LICENSE ---------- Module: github.com/beorn7/perks/quantile @@ -125,9 +125,9 @@ License URL: https://github.com/go-git/go-billy/blob/v5.8.0/LICENSE ---------- Module: github.com/go-git/go-git/v5 -Version: v5.17.0 +Version: v5.17.1 License: Apache-2.0 -License URL: https://github.com/go-git/go-git/blob/v5.17.0/LICENSE +License URL: https://github.com/go-git/go-git/blob/v5.17.1/LICENSE ---------- Module: github.com/go-logr/logr @@ -395,9 +395,9 @@ License URL: https://cs.opensource.google/go/x/net/+/v0.52.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.35.0 +Version: v0.36.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.36.0:LICENSE ---------- Module: golang.org/x/sync/errgroup