From 90313fbf96f81b067d3f64d166c22a0d0b125cff Mon Sep 17 00:00:00 2001 From: Jeff Hubbard Date: Thu, 18 Nov 2021 03:47:48 -0800 Subject: [PATCH] Add idle timeout to `cs create` parameters (#4741) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- internal/codespaces/api/api.go | 32 ++++++++---- pkg/cmd/codespace/create.go | 20 +++++--- pkg/cmd/codespace/create_test.go | 83 ++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index d786bdabb32..01968f264b8 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -461,14 +461,17 @@ func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, loc // CreateCodespaceParams are the required parameters for provisioning a Codespace. type CreateCodespaceParams struct { - RepositoryID int - Branch, Machine, Location string + RepositoryID int + IdleTimeoutMinutes int + Branch string + Machine string + Location string } // CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it // fails to create. func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams) (*Codespace, error) { - codespace, err := a.startCreate(ctx, params.RepositoryID, params.Machine, params.Branch, params.Location) + codespace, err := a.startCreate(ctx, params) if err != errProvisioningInProgress { return codespace, err } @@ -503,10 +506,11 @@ func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams } type startCreateRequest struct { - RepositoryID int `json:"repository_id"` - Ref string `json:"ref"` - Location string `json:"location"` - Machine string `json:"machine"` + RepositoryID int `json:"repository_id"` + IdleTimeoutMinutes int `json:"idle_timeout_minutes"` + Ref string `json:"ref"` + Location string `json:"location"` + Machine string `json:"machine"` } var errProvisioningInProgress = errors.New("provisioning in progress") @@ -515,8 +519,18 @@ var errProvisioningInProgress = errors.New("provisioning in progress") // It may return success or an error, or errProvisioningInProgress indicating that the operation // did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller // must poll the server to learn the outcome. -func (a *API) startCreate(ctx context.Context, repoID int, machine, branch, location string) (*Codespace, error) { - requestBody, err := json.Marshal(startCreateRequest{repoID, branch, location, machine}) +func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*Codespace, error) { + if params == nil { + return nil, errors.New("startCreate missing parameters") + } + + requestBody, err := json.Marshal(startCreateRequest{ + RepositoryID: params.RepositoryID, + IdleTimeoutMinutes: params.IdleTimeoutMinutes, + Ref: params.Branch, + Location: params.Location, + Machine: params.Machine, + }) if err != nil { return nil, fmt.Errorf("error marshaling request: %w", err) } diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index cfc072de2dd..5e2adebc3b1 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/internal/codespaces" @@ -12,10 +13,11 @@ import ( ) type createOptions struct { - repo string - branch string - machine string - showStatus bool + repo string + branch string + machine string + showStatus bool + idleTimeout time.Duration } func newCreateCmd(app *App) *cobra.Command { @@ -34,6 +36,7 @@ func newCreateCmd(app *App) *cobra.Command { createCmd.Flags().StringVarP(&opts.branch, "branch", "b", "", "repository branch") createCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM") createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles") + createCmd.Flags().DurationVar(&opts.idleTimeout, "idle-timeout", 30*time.Minute, "allowed inactivity before codespace is stopped, e.g. \"10m\", \"1h\"") return createCmd } @@ -101,10 +104,11 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { a.StartProgressIndicatorWithLabel("Creating codespace") codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{ - RepositoryID: repository.ID, - Branch: branch, - Machine: machine, - Location: locationResult.Location, + RepositoryID: repository.ID, + Branch: branch, + Machine: machine, + Location: locationResult.Location, + IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), }) a.StopProgressIndicator() if err != nil { diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go index 1a57e4603a0..4b266ffaa87 100644 --- a/pkg/cmd/codespace/create_test.go +++ b/pkg/cmd/codespace/create_test.go @@ -1,9 +1,92 @@ package codespace import ( + "context" + "fmt" "testing" + "time" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" ) +func TestApp_Create(t *testing.T) { + type fields struct { + apiClient apiClient + } + tests := []struct { + name string + fields fields + opts createOptions + wantErr bool + wantStdout string + wantStderr string + }{ + { + name: "create codespace with default branch and 30m idle timeout", + fields: fields{ + apiClient: &apiClientMock{ + GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) { + return "EUROPE", nil + }, + GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) { + return &api.Repository{ + ID: 1234, + FullName: nwo, + DefaultBranch: "main", + }, nil + }, + GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch, location string) ([]*api.Machine, error) { + return []*api.Machine{ + { + Name: "GIGA", + DisplayName: "Gigabits of a machine", + }, + }, nil + }, + CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) { + if params.Branch != "main" { + return nil, fmt.Errorf("got branch %q, want %q", params.Branch, "main") + } + if params.IdleTimeoutMinutes != 30 { + return nil, fmt.Errorf("idle timeout minutes was %v", params.IdleTimeoutMinutes) + } + return &api.Codespace{ + Name: "monalisa-dotfiles-abcd1234", + }, nil + }, + }, + }, + opts: createOptions{ + repo: "monalisa/dotfiles", + branch: "", + machine: "GIGA", + showStatus: false, + idleTimeout: 30 * time.Minute, + }, + wantStdout: "monalisa-dotfiles-abcd1234\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + a := &App{ + io: io, + apiClient: tt.fields.apiClient, + } + if err := a.Create(context.Background(), tt.opts); (err != nil) != tt.wantErr { + t.Errorf("App.Create() error = %v, wantErr %v", err, tt.wantErr) + } + if got := stdout.String(); got != tt.wantStdout { + t.Errorf("stdout = %v, want %v", got, tt.wantStdout) + } + if got := stderr.String(); got != tt.wantStderr { + t.Errorf("stderr = %v, want %v", got, tt.wantStderr) + } + }) + } +} + func TestBuildDisplayName(t *testing.T) { tests := []struct { name string