diff --git a/NOTICE b/NOTICE index c861e91..16e5f1e 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.22.1 +Version: v0.23.2 License: MIT -License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.22.1/gitea/LICENSE +License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.23.2/gitea/LICENSE ---------- Module: dario.cat/mergo @@ -365,9 +365,9 @@ License URL: https://github.com/xanzy/ssh-agent/blob/v0.3.3/LICENSE ---------- Module: gitlab.com/gitlab-org/api/client-go -Version: v1.11.0 +Version: v1.39.0 License: Apache-2.0 -License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.11.0/client-go/LICENSE +License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.39.0/client-go/LICENSE ---------- Module: go.yaml.in/yaml/v2 @@ -401,9 +401,9 @@ License URL: https://cs.opensource.google/go/x/net/+/v0.50.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.34.0 +Version: v0.35.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.34.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE ---------- Module: golang.org/x/sync/errgroup diff --git a/api/workspace.go b/api/workspace.go index 3400139..56977d6 100644 --- a/api/workspace.go +++ b/api/workspace.go @@ -119,6 +119,15 @@ func (c *Client) ScaleWorkspace(wsId int, replicas int) error { return errors.FormatAPIError(r, err) } +// ScaleLandscapeServices scales landscape services by name. +// The services map contains service name -> replica count. +func (c *Client) ScaleLandscapeServices(wsId int, services map[string]int) error { + req := c.api.WorkspacesAPI.WorkspacesScaleLandscapeServices(c.ctx, float32(wsId)). + RequestBody(services) + resp, err := req.Execute() + return errors.FormatAPIError(resp, err) +} + // Waits for a given workspace to be running. // // Returns [TimedOut] error if the workspace does not become running in time. diff --git a/cli/cmd/client.go b/cli/cmd/client.go index c4a49e0..c0a3440 100644 --- a/cli/cmd/client.go +++ b/cli/cmd/client.go @@ -23,6 +23,7 @@ type Client interface { WorkspaceStatus(workspaceId int) (*api.WorkspaceStatus, error) WaitForWorkspaceRunning(workspace *api.Workspace, timeout time.Duration) error ScaleWorkspace(wsId int, replicas int) error + ScaleLandscapeServices(wsId int, services map[string]int) error SetEnvVarOnWorkspace(workspaceId int, vars map[string]string) error ExecCommand(workspaceId int, command string, workdir string, env map[string]string) (string, string, error) ListWorkspacePlans() ([]api.WorkspacePlan, error) diff --git a/cli/cmd/curl.go b/cli/cmd/curl.go index ffbf15b..102d841 100644 --- a/cli/cmd/curl.go +++ b/cli/cmd/curl.go @@ -10,6 +10,7 @@ import ( "log" "os" "os/exec" + "strings" "time" io_pkg "github.com/codesphere-cloud/cs-go/pkg/io" @@ -53,8 +54,17 @@ func (c *CurlCmd) RunE(_ *cobra.Command, args []string) error { return fmt.Errorf("failed to get API token: %w", err) } - path := args[0] - curlArgs := args[1:] + path := "/" + var curlArgs []string + + if len(args) > 0 { + if strings.HasPrefix(args[0], "/") { + path = args[0] + curlArgs = args[1:] + } else { + curlArgs = args + } + } return c.CurlWorkspace(client, wsId, token, path, curlArgs) } @@ -70,9 +80,10 @@ func AddCurlCmd(rootCmd *cobra.Command, opts *GlobalOptions) { {Cmd: "/api/health -w 1234", Desc: "GET request to health endpoint"}, {Cmd: "/api/data -w 1234 -- -XPOST -d '{\"key\":\"value\"}'", Desc: "POST request with data"}, {Cmd: "/api/endpoint -w 1234 -- -v", Desc: "verbose output"}, + {Cmd: "-w 1234 -- -v", Desc: "verbose request to workspace root"}, {Cmd: "/ -- -k", Desc: "skip TLS verification"}, {Cmd: "/ -- -I", Desc: "HEAD request using workspace from env var"}, }), - Args: cobra.MinimumNArgs(1), + Args: cobra.ArbitraryArgs, }, Opts: CurlOptions{ GlobalOptions: opts, @@ -104,8 +115,8 @@ func (c *CurlCmd) CurlWorkspace(client Client, wsId int, token string, path stri ctx, cancel := context.WithTimeout(context.Background(), c.Opts.Timeout) defer cancel() - // Build curl command with authentication header - cmdArgs := []string{"curl", "-H", fmt.Sprintf("x-forward-security: %s", token)} + // Build curl command with authentication header and -L to follow redirects + cmdArgs := []string{"curl", "-L", "-H", fmt.Sprintf("X-CS-Authorization: Bearer %s", token)} cmdArgs = append(cmdArgs, curlArgs...) cmdArgs = append(cmdArgs, url) diff --git a/cli/cmd/curl_test.go b/cli/cmd/curl_test.go index dc11ec0..efdd48b 100644 --- a/cli/cmd/curl_test.go +++ b/cli/cmd/curl_test.go @@ -61,12 +61,16 @@ var _ = Describe("Curl", func() { mock.Anything, "curl", mock.MatchedBy(func(args []string) bool { - // Verify the args contain the expected header, flag, and URL + // Verify the args contain the expected header, flag, -L and URL + hasFollowRedirects := false hasHeader := false hasFlag := false hasURL := false for i, arg := range args { - if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("x-forward-security: %s", token) { + if arg == "-L" { + hasFollowRedirects = true + } + if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("X-CS-Authorization: Bearer %s", token) { hasHeader = true } if arg == "-I" { @@ -76,7 +80,7 @@ var _ = Describe("Curl", func() { hasURL = true } } - return hasHeader && hasFlag && hasURL + return hasFollowRedirects && hasHeader && hasFlag && hasURL }), mock.Anything, mock.Anything, @@ -93,18 +97,22 @@ var _ = Describe("Curl", func() { mock.Anything, "curl", mock.MatchedBy(func(args []string) bool { - // Verify the URL contains the custom path + // Verify the URL contains the custom path and -L flag + hasFollowRedirects := false hasHeader := false hasURL := false for i, arg := range args { - if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("x-forward-security: %s", token) { + if arg == "-L" { + hasFollowRedirects = true + } + if arg == "-H" && i+1 < len(args) && args[i+1] == fmt.Sprintf("X-CS-Authorization: Bearer %s", token) { hasHeader = true } if arg == "https://42-3000.dev.5.codesphere.com/custom/path" { hasURL = true } } - return hasHeader && hasURL + return hasFollowRedirects && hasHeader && hasURL }), mock.Anything, mock.Anything, diff --git a/cli/cmd/mocks.go b/cli/cmd/mocks.go index 5a6f9cf..42246f9 100644 --- a/cli/cmd/mocks.go +++ b/cli/cmd/mocks.go @@ -711,6 +711,63 @@ func (_c *MockClient_ListWorkspaces_Call) RunAndReturn(run func(teamId int) ([]a 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) diff --git a/cli/cmd/wakeup.go b/cli/cmd/wakeup.go index c66ef39..eae64d3 100644 --- a/cli/cmd/wakeup.go +++ b/cli/cmd/wakeup.go @@ -5,6 +5,10 @@ package cmd import ( "fmt" + "log" + "net/http" + "strconv" + "strings" "time" "github.com/codesphere-cloud/cs-go/pkg/io" @@ -13,7 +17,10 @@ import ( type WakeUpOptions struct { *GlobalOptions - Timeout time.Duration + Timeout time.Duration + SyncLandscape bool + Profile string + ScaleServices string // format: "service=replicas,service2=replicas" } type WakeUpCmd struct { @@ -40,11 +47,14 @@ func AddWakeUpCmd(rootCmd *cobra.Command, opts *GlobalOptions) { cmd: &cobra.Command{ Use: "wake-up", Short: "Wake up an on-demand workspace", - Long: `Wake up an on-demand workspace by scaling it to 1 replica via the API.`, + Long: `Wake up an on-demand workspace by scaling it to 1 replica via the API. Optionally syncs the landscape to start services.`, Example: io.FormatExampleCommands("wake-up", []io.Example{ {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 --scale-services web=1,api=2", Desc: "wake up workspace and scale specific services"}, }), }, Opts: WakeUpOptions{ @@ -52,6 +62,9 @@ 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)") + wakeup.cmd.Flags().StringVar(&wakeup.Opts.ScaleServices, "scale-services", "", "Scale specific landscape services (format: 'service=replicas,service2=replicas')") rootCmd.AddCommand(wakeup.cmd) wakeup.cmd.RunE = wakeup.RunE } @@ -68,31 +81,131 @@ func (c *WakeUpCmd) WakeUpWorkspace(client Client, wsId int) error { return fmt.Errorf("failed to get workspace status: %w", err) } - if status.IsRunning { - fmt.Printf("Workspace %d (%s) is already running\n", wsId, workspace.Name) - return nil + 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) + } + } + + 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) } - fmt.Printf("Waking up workspace %d (%s)...\n", wsId, workspace.Name) + if c.Opts.ScaleServices != "" { + services, err := parseScaleServices(c.Opts.ScaleServices) + if err != nil { + return fmt.Errorf("failed to parse scale-services: %w", err) + } + log.Printf("Scaling landscape services for workspace %d: %v\n", wsId, services) + err = client.ScaleLandscapeServices(wsId, services) + if err != nil { + return fmt.Errorf("failed to scale landscape services: %w", err) + } + log.Printf("Landscape services scaled for workspace %d\n", wsId) + } - // 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 + if workspace.DevDomain == nil || *workspace.DevDomain == "" { + log.Printf("Workspace %d does not have a dev domain, skipping health check\n", wsId) + return nil } - err = client.ScaleWorkspace(wsId, targetReplicas) + 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 scale workspace: %w", err) + return fmt.Errorf("failed to get API token: %w", err) } - fmt.Printf("Waiting for workspace %d to be running...\n", wsId) - err = client.WaitForWorkspaceRunning(&workspace, c.Opts.Timeout) + err = c.waitForWorkspaceHealthy(*workspace.DevDomain, token, c.Opts.Timeout) if err != nil { - return fmt.Errorf("workspace did not become running: %w", err) + return fmt.Errorf("workspace did not become healthy: %w", err) } - fmt.Printf("Workspace %d is now running\n", wsId) + 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 { + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode == http.StatusOK { + return nil + } + log.Printf("Workspace %s responded with status code %d, retrying...\n", devDomain, resp.StatusCode) + } + + if time.Now().After(maxWaitTime) { + return fmt.Errorf("timeout waiting for workspace to be healthy at %s", url) + } + + time.Sleep(delay) + } +} + +// parseScaleServices parses a string like "web=1,api=2" into a map[string]int +func parseScaleServices(s string) (map[string]int, error) { + result := make(map[string]int) + if s == "" { + return result, nil + } + + pairs := strings.Split(s, ",") + for _, pair := range pairs { + parts := strings.SplitN(strings.TrimSpace(pair), "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid format '%s', expected 'service=replicas'", pair) + } + service := strings.TrimSpace(parts[0]) + if service == "" { + return nil, fmt.Errorf("empty service name in '%s'", pair) + } + replicas, err := strconv.Atoi(strings.TrimSpace(parts[1])) + if err != nil { + return nil, fmt.Errorf("invalid replica count '%s' for service '%s': %w", parts[1], service, err) + } + if replicas < 1 { + return nil, fmt.Errorf("replica count must be at least 1 for service '%s'", service) + } + result[service] = replicas + } + return result, nil +} diff --git a/cli/cmd/wakeup_test.go b/cli/cmd/wakeup_test.go index b9ab8b0..34d34d2 100644 --- a/cli/cmd/wakeup_test.go +++ b/cli/cmd/wakeup_test.go @@ -98,5 +98,160 @@ var _ = Describe("WakeUp", func() { 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()) + }) + + It("should scale landscape services when ScaleServices flag is set", func() { + workspace := api.Workspace{ + Id: wsId, + TeamId: teamId, + Name: "test-workspace", + } + c.Opts.ScaleServices = "web=1,api=2" + + 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().ScaleLandscapeServices(wsId, map[string]int{"web": 1, "api": 2}).Return(nil) + + err := c.WakeUpWorkspace(mockClient, wsId) + + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return error if ScaleLandscapeServices fails", func() { + workspace := api.Workspace{ + Id: wsId, + TeamId: teamId, + Name: "test-workspace", + } + c.Opts.ScaleServices = "web=1" + + 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().ScaleLandscapeServices(wsId, map[string]int{"web": 1}).Return(fmt.Errorf("scale services error")) + + err := c.WakeUpWorkspace(mockClient, wsId) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to scale landscape services")) + }) + + It("should return error for invalid scale-services format", func() { + workspace := api.Workspace{ + Id: wsId, + TeamId: teamId, + Name: "test-workspace", + } + c.Opts.ScaleServices = "invalid" + + 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).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse scale-services")) + }) + + It("should use both sync-landscape and scale-services together", func() { + workspace := api.Workspace{ + Id: wsId, + TeamId: teamId, + Name: "test-workspace", + } + c.Opts.SyncLandscape = true + c.Opts.ScaleServices = "web=1" + + 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) + mockClient.EXPECT().ScaleLandscapeServices(wsId, map[string]int{"web": 1}).Return(nil) + + err := c.WakeUpWorkspace(mockClient, wsId) + + Expect(err).ToNot(HaveOccurred()) + }) }) }) diff --git a/docs/cs_curl.md b/docs/cs_curl.md index 5bf8d07..73bd022 100644 --- a/docs/cs_curl.md +++ b/docs/cs_curl.md @@ -25,6 +25,9 @@ $ cs curl /api/data -w 1234 -- -XPOST -d '{"key":"value"}' # verbose output $ cs curl /api/endpoint -w 1234 -- -v +# verbose request to workspace root +$ cs curl -w 1234 -- -v + # skip TLS verification $ cs curl / -- -k diff --git a/docs/cs_wake-up.md b/docs/cs_wake-up.md index fa746d5..478f373 100644 --- a/docs/cs_wake-up.md +++ b/docs/cs_wake-up.md @@ -4,7 +4,7 @@ Wake up an on-demand workspace ### Synopsis -Wake up an on-demand workspace by scaling it to 1 replica via the API. +Wake up an on-demand workspace by scaling it to 1 replica via the API. Optionally syncs the landscape to start services. ``` cs wake-up [flags] @@ -21,13 +21,25 @@ $ cs wake-up # wake up workspace with 60 second timeout $ cs wake-up -w 1234 --timeout 60s + +# wake up workspace and deploy landscape from CI profile +$ cs wake-up -w 1234 --sync-landscape + +# wake up workspace and deploy landscape with prod profile +$ cs wake-up -w 1234 --sync-landscape --profile prod + +# wake up workspace and scale specific services +$ cs wake-up -w 1234 --scale-services web=1,api=2 ``` ### Options ``` - -h, --help help for wake-up - --timeout duration Timeout for waking up the workspace (default 2m0s) + -h, --help help for wake-up + -p, --profile string CI profile to use for landscape deploy (e.g. 'prod' for ci.prod.yml) + --scale-services string Scale specific landscape services (format: 'service=replicas,service2=replicas') + --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 diff --git a/pkg/tmpl/NOTICE b/pkg/tmpl/NOTICE index c861e91..16e5f1e 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.22.1 +Version: v0.23.2 License: MIT -License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.22.1/gitea/LICENSE +License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.23.2/gitea/LICENSE ---------- Module: dario.cat/mergo @@ -365,9 +365,9 @@ License URL: https://github.com/xanzy/ssh-agent/blob/v0.3.3/LICENSE ---------- Module: gitlab.com/gitlab-org/api/client-go -Version: v1.11.0 +Version: v1.39.0 License: Apache-2.0 -License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.11.0/client-go/LICENSE +License URL: https://gitlab.com/gitlab-org/api/blob/client-go/v1.39.0/client-go/LICENSE ---------- Module: go.yaml.in/yaml/v2 @@ -401,9 +401,9 @@ License URL: https://cs.opensource.google/go/x/net/+/v0.50.0:LICENSE ---------- Module: golang.org/x/oauth2 -Version: v0.34.0 +Version: v0.35.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/oauth2/+/v0.34.0:LICENSE +License URL: https://cs.opensource.google/go/x/oauth2/+/v0.35.0:LICENSE ---------- Module: golang.org/x/sync/errgroup