From 062595661e0c04214dbbd9f73b80795f7ceb78e2 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 6 Feb 2025 13:06:12 +0000 Subject: [PATCH 1/6] add sandbox create --- .devcontainer/Dockerfile | 23 ------- .devcontainer/devcontainer.json | 23 ++----- lib/sandbox/sandbox.go | 113 ++++++++++++++++++++++++++++++++ lib/sandbox/sandbox_test.go | 7 ++ 4 files changed, 126 insertions(+), 40 deletions(-) delete mode 100644 .devcontainer/Dockerfile create mode 100644 lib/sandbox/sandbox.go create mode 100644 lib/sandbox/sandbox_test.go diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 1aa883d..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# syntax=docker/dockerfile:1 -FROM debian:bookworm-slim - -RUN apt-get update && apt-get install -y \ - libxkbcommon0 \ - ca-certificates \ - git \ - golang \ - unzip \ - libc++1 \ - vim \ - && apt-get clean autoclean - -# Ensure UTF-8 encoding -ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 - -ENV GOPATH=/go -ENV PATH=$GOPATH/bin:$PATH - -WORKDIR /workspace - -COPY . /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d55fc4d..8fe444a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,9 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/debian { - "name": "Debian", - "build": { - "dockerfile": "Dockerfile" + "name": "Gitpod Flex - Go SDK", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.23" + } } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" } diff --git a/lib/sandbox/sandbox.go b/lib/sandbox/sandbox.go new file mode 100644 index 0000000..33afcd8 --- /dev/null +++ b/lib/sandbox/sandbox.go @@ -0,0 +1,113 @@ +package sandbox + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/stainless-sdks/gitpod-go" +) + +type EnvironmentSandbox struct { + Client *gitpod.Client +} + +func NewEnvironmentSandbox(client *gitpod.Client) *EnvironmentSandbox { + return &EnvironmentSandbox{ + Client: client, + } +} + +type CreateEnvironmentParams struct { + ProjectID string + ContextURL string + EnvironmentClass string +} + +func (s *EnvironmentSandbox) Create(ctx context.Context, params *CreateEnvironmentParams) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { + envID, err := s.create(ctx, params) + if err != nil { + return nil, err + } + return s.waitForRunning(ctx, envID) +} + +func (s *EnvironmentSandbox) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + tick := time.NewTicker(500 * time.Microsecond) + defer tick.Stop() + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-tick.C: + resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ + EnvironmentID: gitpod.String(envID), + }) + if err != nil { + // TODO: if transient we should retry? + return nil, err + } + + if fm := resp.Environment.Status.FailureMessage; len(fm) > 0 { + return nil, fmt.Errorf("environment creation failed: %s", fm) + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseRunning { + return &resp.Environment, nil + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopping { + return nil, errors.New("environment creation failed: environment is stopping") + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopped { + return nil, errors.New("environment creation failed: environment is stopped") + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleting { + return nil, errors.New("environment creation failed: environment is deleting") + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleted { + return nil, errors.New("environment creation failed: environment is deleted") + } + } + } +} + +func (s *EnvironmentSandbox) create(ctx context.Context, params *CreateEnvironmentParams) (string, error) { + if params.ProjectID != "" { + resp, err := s.Client.Environments.NewFromProject(ctx, gitpod.EnvironmentNewFromProjectParams{ + ProjectID: gitpod.String(params.ProjectID), + }) + if err != nil { + return "", err + } + return resp.Environment.ID, nil + } + if params.ContextURL != "" { + if params.EnvironmentClass == "" { + return "", errors.New("environmentClass must be provided when contextURL is provided") + } + resp, err := s.Client.Environments.New(ctx, gitpod.EnvironmentNewParams{ + Spec: gitpod.F(gitpod.EnvironmentNewParamsSpec{ + DesiredPhase: gitpod.F(gitpod.EnvironmentNewParamsSpecDesiredPhaseEnvironmentPhaseRunning), + Machine: gitpod.F(gitpod.EnvironmentNewParamsSpecMachine{ + Class: gitpod.String(params.EnvironmentClass), + }), + Content: gitpod.F(gitpod.EnvironmentNewParamsSpecContent{ + Initializer: gitpod.F(gitpod.EnvironmentNewParamsSpecContentInitializer{ + Specs: gitpod.F([]gitpod.EnvironmentNewParamsSpecContentInitializerSpecUnion{ + gitpod.EnvironmentNewParamsSpecContentInitializerSpecsContextURL{ + ContextURL: gitpod.F(gitpod.EnvironmentNewParamsSpecContentInitializerSpecsContextURLContextURL{ + URL: gitpod.String(params.ContextURL), + }), + }, + }), + }), + }), + }), + }) + if err != nil { + return "", err + } + return resp.Environment.ID, nil + } + return "", errors.New("either projectID or contextURL must be provided") +} diff --git a/lib/sandbox/sandbox_test.go b/lib/sandbox/sandbox_test.go new file mode 100644 index 0000000..ee7e827 --- /dev/null +++ b/lib/sandbox/sandbox_test.go @@ -0,0 +1,7 @@ +package sandbox + +import "testing" + +func TestSandbox(t *testing.T) { + +} From 9a137bee7a3b67d7c8c5c34a39c19605f200dadc Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 6 Feb 2025 13:36:51 +0000 Subject: [PATCH 2/6] feat(api): enhance environment management with create, start, stop, and delete options --- lib/sandbox/sandbox.go | 185 ++++++++++++++++++++++++++++++++--------- 1 file changed, 146 insertions(+), 39 deletions(-) diff --git a/lib/sandbox/sandbox.go b/lib/sandbox/sandbox.go index 33afcd8..5bb857f 100644 --- a/lib/sandbox/sandbox.go +++ b/lib/sandbox/sandbox.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" - "time" + "net/http" "github.com/stainless-sdks/gitpod-go" + "github.com/stainless-sdks/gitpod-go/internal/apierror" ) type EnvironmentSandbox struct { @@ -19,84 +20,190 @@ func NewEnvironmentSandbox(client *gitpod.Client) *EnvironmentSandbox { } } -type CreateEnvironmentParams struct { +type CreateEnvironmentOptions struct { ProjectID string ContextURL string EnvironmentClass string } -func (s *EnvironmentSandbox) Create(ctx context.Context, params *CreateEnvironmentParams) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { - envID, err := s.create(ctx, params) +func (s *EnvironmentSandbox) Create(ctx context.Context, options *CreateEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { + envID, err := s.create(ctx, options) if err != nil { return nil, err } return s.waitForRunning(ctx, envID) } +type StartEnvironmentOptions struct { + EnvironmentID string +} + +func (s *EnvironmentSandbox) Start(ctx context.Context, options *StartEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { + _, err = s.Client.Environments.Start(ctx, gitpod.EnvironmentStartParams{ + EnvironmentID: gitpod.String(options.EnvironmentID), + }) + if err != nil { + return nil, err + } + return s.waitForRunning(ctx, options.EnvironmentID) +} + +type StopEnvironmentOptions struct { + EnvironmentID string +} + +func (s *EnvironmentSandbox) Stop(ctx context.Context, options *StopEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { + _, err = s.Client.Environments.Stop(ctx, gitpod.EnvironmentStopParams{ + EnvironmentID: gitpod.String(options.EnvironmentID), + }) + if err != nil { + return nil, err + } + return s.waitForStopped(ctx, options.EnvironmentID) +} + +type DeleteEnvironmentOptions struct { + EnvironmentID string +} + +func (s *EnvironmentSandbox) Delete(ctx context.Context, options *DeleteEnvironmentOptions) error { + _, err := s.Client.Environments.Delete(ctx, gitpod.EnvironmentDeleteParams{ + EnvironmentID: gitpod.String(options.EnvironmentID), + }) + if err != nil { + apierr, ok := err.(*apierror.Error) + if ok && apierr.StatusCode == http.StatusNotFound { + return nil + } + return err + } + return s.waitForDeleted(ctx, options.EnvironmentID) +} + +func (s *EnvironmentSandbox) waitForDeleted(ctx context.Context, envID string) error { + _, err := s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { + resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ + EnvironmentID: gitpod.String(envID), + }) + if err != nil { + apierr, ok := err.(*apierror.Error) + if ok && apierr.StatusCode == http.StatusNotFound { + return nil, true, nil + } + return nil, false, err + } + return nil, resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleted, nil + }) + return err +} + +func (s *EnvironmentSandbox) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { + resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ + EnvironmentID: gitpod.String(envID), + }) + if err != nil { + return nil, false, err + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopped || + resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleting || + resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleted { + return &resp.Environment, true, nil + } + return nil, false, nil + }) +} + func (s *EnvironmentSandbox) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { - tick := time.NewTicker(500 * time.Microsecond) - defer tick.Stop() - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-tick.C: - resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ - EnvironmentID: gitpod.String(envID), - }) + return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { + resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ + EnvironmentID: gitpod.String(envID), + }) + if err != nil { + return nil, false, err + } + + if fm := resp.Environment.Status.FailureMessage; len(fm) > 0 { + return nil, false, fmt.Errorf("environment creation failed: %s", fm) + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseRunning { + return &resp.Environment, true, nil + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopping { + return nil, false, errors.New("environment creation failed: environment is stopping") + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopped { + return nil, false, errors.New("environment creation failed: environment is stopped") + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleting { + return nil, false, errors.New("environment creation failed: environment is deleting") + } + if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleted { + return nil, false, errors.New("environment creation failed: environment is deleted") + } + return nil, false, nil + }) +} + +func (s *EnvironmentSandbox) waitFor(ctx context.Context, envID string, fetch func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { + env, ok, err := fetch() + if err != nil { + return nil, err + } + if ok { + return env, nil + } + + stream := s.Client.Events.WatchStreaming(ctx, gitpod.EventWatchParams{ + Body: gitpod.EventWatchParamsBodyEnvironmentScopeProducesEventsForTheEnvironmentItselfAllTasksTaskExecutionsAndServicesAssociatedWithThatEnvironment{ + EnvironmentID: gitpod.String(envID), + }, + }) + defer stream.Close() + + for stream.Next() { + resp := stream.Current() + if resp.ResourceType == gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment && resp.ResourceID == envID { + env, ok, err := fetch() if err != nil { // TODO: if transient we should retry? return nil, err } - - if fm := resp.Environment.Status.FailureMessage; len(fm) > 0 { - return nil, fmt.Errorf("environment creation failed: %s", fm) - } - if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseRunning { - return &resp.Environment, nil - } - if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopping { - return nil, errors.New("environment creation failed: environment is stopping") - } - if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopped { - return nil, errors.New("environment creation failed: environment is stopped") - } - if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleting { - return nil, errors.New("environment creation failed: environment is deleting") - } - if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseDeleted { - return nil, errors.New("environment creation failed: environment is deleted") + if ok { + return env, nil } } } + + return nil, stream.Err() } -func (s *EnvironmentSandbox) create(ctx context.Context, params *CreateEnvironmentParams) (string, error) { - if params.ProjectID != "" { +func (s *EnvironmentSandbox) create(ctx context.Context, options *CreateEnvironmentOptions) (string, error) { + if options.ProjectID != "" { resp, err := s.Client.Environments.NewFromProject(ctx, gitpod.EnvironmentNewFromProjectParams{ - ProjectID: gitpod.String(params.ProjectID), + ProjectID: gitpod.String(options.ProjectID), }) if err != nil { return "", err } return resp.Environment.ID, nil } - if params.ContextURL != "" { - if params.EnvironmentClass == "" { + if options.ContextURL != "" { + if options.EnvironmentClass == "" { return "", errors.New("environmentClass must be provided when contextURL is provided") } resp, err := s.Client.Environments.New(ctx, gitpod.EnvironmentNewParams{ Spec: gitpod.F(gitpod.EnvironmentNewParamsSpec{ DesiredPhase: gitpod.F(gitpod.EnvironmentNewParamsSpecDesiredPhaseEnvironmentPhaseRunning), Machine: gitpod.F(gitpod.EnvironmentNewParamsSpecMachine{ - Class: gitpod.String(params.EnvironmentClass), + Class: gitpod.String(options.EnvironmentClass), }), Content: gitpod.F(gitpod.EnvironmentNewParamsSpecContent{ Initializer: gitpod.F(gitpod.EnvironmentNewParamsSpecContentInitializer{ Specs: gitpod.F([]gitpod.EnvironmentNewParamsSpecContentInitializerSpecUnion{ gitpod.EnvironmentNewParamsSpecContentInitializerSpecsContextURL{ ContextURL: gitpod.F(gitpod.EnvironmentNewParamsSpecContentInitializerSpecsContextURLContextURL{ - URL: gitpod.String(params.ContextURL), + URL: gitpod.String(options.ContextURL), }), }, }), From 09b2691a4080829ad19be61e3612bc814eb958c1 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 6 Feb 2025 13:53:38 +0000 Subject: [PATCH 3/6] feat(api): implement environment management interface with create, start, stop, and delete methods --- lib/sandbox/sandbox.go | 80 +++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/lib/sandbox/sandbox.go b/lib/sandbox/sandbox.go index 5bb857f..52ade01 100644 --- a/lib/sandbox/sandbox.go +++ b/lib/sandbox/sandbox.go @@ -10,23 +10,58 @@ import ( "github.com/stainless-sdks/gitpod-go/internal/apierror" ) -type EnvironmentSandbox struct { +type CreateEnvironmentOptions struct { + ProjectID string + ContextURL string + EnvironmentClass string +} + +type StartEnvironmentOptions struct { + EnvironmentID string +} + +type StopEnvironmentOptions struct { + EnvironmentID string +} + +type DeleteEnvironmentOptions struct { + EnvironmentID string +} + +type EnvironmentSandbox interface { + // Get fetches an existing environment. + Get(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) + // Create creates a new environment and waits for it to be running. + Create(ctx context.Context, options *CreateEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) + // Start starts an existing environment and waits for it to be running. + Start(ctx context.Context, options *StartEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) + // Stop stops an existing environment and waits for it to be stopped. + Stop(ctx context.Context, options *StopEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) + // Delete deletes an existing environment and waits for it to be deleted. + Delete(ctx context.Context, options *DeleteEnvironmentOptions) error +} + +type environmentSandbox struct { Client *gitpod.Client } -func NewEnvironmentSandbox(client *gitpod.Client) *EnvironmentSandbox { - return &EnvironmentSandbox{ +func NewEnvironmentSandbox(client *gitpod.Client) EnvironmentSandbox { + return &environmentSandbox{ Client: client, } } -type CreateEnvironmentOptions struct { - ProjectID string - ContextURL string - EnvironmentClass string +func (s *environmentSandbox) Get(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ + EnvironmentID: gitpod.String(envID), + }) + if err != nil { + return nil, err + } + return &resp.Environment, nil } -func (s *EnvironmentSandbox) Create(ctx context.Context, options *CreateEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { +func (s *environmentSandbox) Create(ctx context.Context, options *CreateEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { envID, err := s.create(ctx, options) if err != nil { return nil, err @@ -34,11 +69,7 @@ func (s *EnvironmentSandbox) Create(ctx context.Context, options *CreateEnvironm return s.waitForRunning(ctx, envID) } -type StartEnvironmentOptions struct { - EnvironmentID string -} - -func (s *EnvironmentSandbox) Start(ctx context.Context, options *StartEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { +func (s *environmentSandbox) Start(ctx context.Context, options *StartEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { _, err = s.Client.Environments.Start(ctx, gitpod.EnvironmentStartParams{ EnvironmentID: gitpod.String(options.EnvironmentID), }) @@ -48,11 +79,7 @@ func (s *EnvironmentSandbox) Start(ctx context.Context, options *StartEnvironmen return s.waitForRunning(ctx, options.EnvironmentID) } -type StopEnvironmentOptions struct { - EnvironmentID string -} - -func (s *EnvironmentSandbox) Stop(ctx context.Context, options *StopEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { +func (s *environmentSandbox) Stop(ctx context.Context, options *StopEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { _, err = s.Client.Environments.Stop(ctx, gitpod.EnvironmentStopParams{ EnvironmentID: gitpod.String(options.EnvironmentID), }) @@ -61,12 +88,7 @@ func (s *EnvironmentSandbox) Stop(ctx context.Context, options *StopEnvironmentO } return s.waitForStopped(ctx, options.EnvironmentID) } - -type DeleteEnvironmentOptions struct { - EnvironmentID string -} - -func (s *EnvironmentSandbox) Delete(ctx context.Context, options *DeleteEnvironmentOptions) error { +func (s *environmentSandbox) Delete(ctx context.Context, options *DeleteEnvironmentOptions) error { _, err := s.Client.Environments.Delete(ctx, gitpod.EnvironmentDeleteParams{ EnvironmentID: gitpod.String(options.EnvironmentID), }) @@ -80,7 +102,7 @@ func (s *EnvironmentSandbox) Delete(ctx context.Context, options *DeleteEnvironm return s.waitForDeleted(ctx, options.EnvironmentID) } -func (s *EnvironmentSandbox) waitForDeleted(ctx context.Context, envID string) error { +func (s *environmentSandbox) waitForDeleted(ctx context.Context, envID string) error { _, err := s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), @@ -97,7 +119,7 @@ func (s *EnvironmentSandbox) waitForDeleted(ctx context.Context, envID string) e return err } -func (s *EnvironmentSandbox) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { +func (s *environmentSandbox) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), @@ -114,7 +136,7 @@ func (s *EnvironmentSandbox) waitForStopped(ctx context.Context, envID string) ( }) } -func (s *EnvironmentSandbox) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { +func (s *environmentSandbox) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), @@ -145,7 +167,7 @@ func (s *EnvironmentSandbox) waitForRunning(ctx context.Context, envID string) ( }) } -func (s *EnvironmentSandbox) waitFor(ctx context.Context, envID string, fetch func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { +func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { env, ok, err := fetch() if err != nil { return nil, err @@ -178,7 +200,7 @@ func (s *EnvironmentSandbox) waitFor(ctx context.Context, envID string, fetch fu return nil, stream.Err() } -func (s *EnvironmentSandbox) create(ctx context.Context, options *CreateEnvironmentOptions) (string, error) { +func (s *environmentSandbox) create(ctx context.Context, options *CreateEnvironmentOptions) (string, error) { if options.ProjectID != "" { resp, err := s.Client.Environments.NewFromProject(ctx, gitpod.EnvironmentNewFromProjectParams{ ProjectID: gitpod.String(options.ProjectID), From 2af77d0a5fcd2069f0af872b4a7ba8bd98268ce8 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 6 Feb 2025 16:06:45 +0000 Subject: [PATCH 4/6] feat(api): implement sandbox management with execution and logging capabilities --- lib/{sandbox/sandbox.go => environment.go} | 103 ++++++++------------ lib/sandbox.go | 105 +++++++++++++++++++++ lib/sandbox/sandbox_test.go | 7 -- 3 files changed, 145 insertions(+), 70 deletions(-) rename lib/{sandbox/sandbox.go => environment.go} (58%) create mode 100644 lib/sandbox.go delete mode 100644 lib/sandbox/sandbox_test.go diff --git a/lib/sandbox/sandbox.go b/lib/environment.go similarity index 58% rename from lib/sandbox/sandbox.go rename to lib/environment.go index 52ade01..e658d1e 100644 --- a/lib/sandbox/sandbox.go +++ b/lib/environment.go @@ -1,4 +1,4 @@ -package sandbox +package lib import ( "context" @@ -10,58 +10,30 @@ import ( "github.com/stainless-sdks/gitpod-go/internal/apierror" ) -type CreateEnvironmentOptions struct { +type CreateOptions struct { ProjectID string ContextURL string EnvironmentClass string } -type StartEnvironmentOptions struct { - EnvironmentID string +type EnvironmentService interface { + Create(ctx context.Context, options *CreateOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) + Start(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) + Stop(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) + Delete(ctx context.Context, environmentID string) error } -type StopEnvironmentOptions struct { - EnvironmentID string -} - -type DeleteEnvironmentOptions struct { - EnvironmentID string -} - -type EnvironmentSandbox interface { - // Get fetches an existing environment. - Get(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) - // Create creates a new environment and waits for it to be running. - Create(ctx context.Context, options *CreateEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) - // Start starts an existing environment and waits for it to be running. - Start(ctx context.Context, options *StartEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) - // Stop stops an existing environment and waits for it to be stopped. - Stop(ctx context.Context, options *StopEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) - // Delete deletes an existing environment and waits for it to be deleted. - Delete(ctx context.Context, options *DeleteEnvironmentOptions) error -} - -type environmentSandbox struct { - Client *gitpod.Client -} - -func NewEnvironmentSandbox(client *gitpod.Client) EnvironmentSandbox { - return &environmentSandbox{ +func NewEnvironmentService(client *gitpod.Client) EnvironmentService { + return &environmentService{ Client: client, } } -func (s *environmentSandbox) Get(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { - resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ - EnvironmentID: gitpod.String(envID), - }) - if err != nil { - return nil, err - } - return &resp.Environment, nil +type environmentService struct { + Client *gitpod.Client } -func (s *environmentSandbox) Create(ctx context.Context, options *CreateEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { +func (s *environmentService) Create(ctx context.Context, options *CreateOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) { envID, err := s.create(ctx, options) if err != nil { return nil, err @@ -69,28 +41,29 @@ func (s *environmentSandbox) Create(ctx context.Context, options *CreateEnvironm return s.waitForRunning(ctx, envID) } -func (s *environmentSandbox) Start(ctx context.Context, options *StartEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { - _, err = s.Client.Environments.Start(ctx, gitpod.EnvironmentStartParams{ - EnvironmentID: gitpod.String(options.EnvironmentID), +func (s *environmentService) Start(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + _, err := s.Client.Environments.Start(ctx, gitpod.EnvironmentStartParams{ + EnvironmentID: gitpod.String(environmentID), }) if err != nil { return nil, err } - return s.waitForRunning(ctx, options.EnvironmentID) + return s.waitForRunning(ctx, environmentID) } -func (s *environmentSandbox) Stop(ctx context.Context, options *StopEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) { - _, err = s.Client.Environments.Stop(ctx, gitpod.EnvironmentStopParams{ - EnvironmentID: gitpod.String(options.EnvironmentID), +func (s *environmentService) Stop(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + _, err := s.Client.Environments.Stop(ctx, gitpod.EnvironmentStopParams{ + EnvironmentID: gitpod.String(environmentID), }) if err != nil { return nil, err } - return s.waitForStopped(ctx, options.EnvironmentID) + return s.waitForStopped(ctx, environmentID) } -func (s *environmentSandbox) Delete(ctx context.Context, options *DeleteEnvironmentOptions) error { + +func (s *environmentService) Delete(ctx context.Context, environmentID string) error { _, err := s.Client.Environments.Delete(ctx, gitpod.EnvironmentDeleteParams{ - EnvironmentID: gitpod.String(options.EnvironmentID), + EnvironmentID: gitpod.String(environmentID), }) if err != nil { apierr, ok := err.(*apierror.Error) @@ -99,11 +72,11 @@ func (s *environmentSandbox) Delete(ctx context.Context, options *DeleteEnvironm } return err } - return s.waitForDeleted(ctx, options.EnvironmentID) + return s.waitForDeleted(ctx, environmentID) } -func (s *environmentSandbox) waitForDeleted(ctx context.Context, envID string) error { - _, err := s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { +func (s *environmentService) waitForDeleted(ctx context.Context, envID string) error { + _, err := s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), }) @@ -119,8 +92,8 @@ func (s *environmentSandbox) waitForDeleted(ctx context.Context, envID string) e return err } -func (s *environmentSandbox) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { - return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { +func (s *environmentService) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), }) @@ -136,8 +109,8 @@ func (s *environmentSandbox) waitForStopped(ctx context.Context, envID string) ( }) } -func (s *environmentSandbox) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { - return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { +func (s *environmentService) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), }) @@ -167,8 +140,12 @@ func (s *environmentSandbox) waitForRunning(ctx context.Context, envID string) ( }) } -func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { - env, ok, err := fetch() +func (s *environmentService) waitForEnvironment(ctx context.Context, envID string, check func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return waitFor(ctx, s.Client, envID, gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment, envID, check) +} + +func waitFor[T any](ctx context.Context, client *gitpod.Client, envID string, resourceType gitpod.EventWatchResponseResourceType, resourceID string, check func() (*T, bool, error)) (*T, error) { + env, ok, err := check() if err != nil { return nil, err } @@ -176,7 +153,7 @@ func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch fu return env, nil } - stream := s.Client.Events.WatchStreaming(ctx, gitpod.EventWatchParams{ + stream := client.Events.WatchStreaming(ctx, gitpod.EventWatchParams{ Body: gitpod.EventWatchParamsBodyEnvironmentScopeProducesEventsForTheEnvironmentItselfAllTasksTaskExecutionsAndServicesAssociatedWithThatEnvironment{ EnvironmentID: gitpod.String(envID), }, @@ -185,8 +162,8 @@ func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch fu for stream.Next() { resp := stream.Current() - if resp.ResourceType == gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment && resp.ResourceID == envID { - env, ok, err := fetch() + if resp.ResourceType == resourceType && resp.ResourceID == resourceID { + env, ok, err := check() if err != nil { // TODO: if transient we should retry? return nil, err @@ -200,7 +177,7 @@ func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch fu return nil, stream.Err() } -func (s *environmentSandbox) create(ctx context.Context, options *CreateEnvironmentOptions) (string, error) { +func (s *environmentService) create(ctx context.Context, options *CreateOptions) (string, error) { if options.ProjectID != "" { resp, err := s.Client.Environments.NewFromProject(ctx, gitpod.EnvironmentNewFromProjectParams{ ProjectID: gitpod.String(options.ProjectID), diff --git a/lib/sandbox.go b/lib/sandbox.go new file mode 100644 index 0000000..de2639c --- /dev/null +++ b/lib/sandbox.go @@ -0,0 +1,105 @@ +package lib + +import ( + "context" + "fmt" + "io" + "math/rand/v2" + "net/http" + + "github.com/stainless-sdks/gitpod-go" +) + +type Sandbox interface { + io.Closer + Exec(ctx context.Context, command string) (io.ReadCloser, error) +} + +func NewSandbox(ctx context.Context, client *gitpod.Client, environmentID string) (Sandbox, error) { + logTokenResp, err := client.Environments.NewLogsToken(ctx, gitpod.EnvironmentNewLogsTokenParams{ + EnvironmentID: gitpod.String(environmentID), + }) + if err != nil { + return nil, err + } + + taskReference := fmt.Sprintf("sandbox-%d", rand.Uint64()) + resp, err := client.Environments.Automations.Tasks.New(ctx, gitpod.EnvironmentAutomationTaskNewParams{ + EnvironmentID: gitpod.String(environmentID), + Metadata: gitpod.F(gitpod.EnvironmentAutomationTaskNewParamsMetadata{ + Reference: gitpod.String(taskReference), + Name: gitpod.String("Sandbox Task"), + Description: gitpod.String("Sandbox task"), + }), + }) + if err != nil { + return nil, nil + } + + return &sandbox{ + taskID: resp.Task.ID, + environmentID: environmentID, + logAccessToken: logTokenResp.AccessToken, + client: client, + }, nil +} + +type sandbox struct { + taskID string + environmentID string + logAccessToken string + client *gitpod.Client +} + +func (s *sandbox) Close() error { + _, err := s.client.Environments.Automations.Tasks.Delete(context.Background(), gitpod.EnvironmentAutomationTaskDeleteParams{ + ID: gitpod.String(s.taskID), + }) + return err +} + +func (s *sandbox) Exec(ctx context.Context, command string) (io.ReadCloser, error) { + _, err := s.client.Environments.Automations.Tasks.Update(ctx, gitpod.EnvironmentAutomationTaskUpdateParams{ + ID: gitpod.String(s.taskID), + Spec: gitpod.F[gitpod.EnvironmentAutomationTaskUpdateParamsSpecUnion](gitpod.EnvironmentAutomationTaskUpdateParamsSpecCommand{ + Command: gitpod.String(command), + }), + }) + if err != nil { + return nil, err + } + startResp, err := s.client.Environments.Automations.Tasks.Start(ctx, gitpod.EnvironmentAutomationTaskStartParams{ + ID: gitpod.String(s.taskID), + }) + if err != nil { + return nil, err + } + taskExecutionID := startResp.TaskExecution.ID + taskExecution, err := waitFor(ctx, s.client, s.environmentID, gitpod.EventWatchResponseResourceTypeResourceTypeTaskExecution, taskExecutionID, func() (*gitpod.EnvironmentAutomationTaskExecutionGetResponseTaskExecution, bool, error) { + resp, err := s.client.Environments.Automations.Tasks.Executions.Get(ctx, gitpod.EnvironmentAutomationTaskExecutionGetParams{ + ID: gitpod.String(taskExecutionID), + }) + if err != nil { + return nil, false, err + } + if resp.TaskExecution.Status.LogURL != "" { + return &resp.TaskExecution, true, nil + } + return nil, false, nil + }) + if err != nil { + return nil, err + } + logURL := taskExecution.Status.LogURL + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, logURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.logAccessToken)) + logResp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return logResp.Body, nil +} diff --git a/lib/sandbox/sandbox_test.go b/lib/sandbox/sandbox_test.go deleted file mode 100644 index ee7e827..0000000 --- a/lib/sandbox/sandbox_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package sandbox - -import "testing" - -func TestSandbox(t *testing.T) { - -} From b61eb0071af84f72248ab815e985c2ec6524186f Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 6 Feb 2025 16:30:05 +0000 Subject: [PATCH 5/6] feat(api): enhance sandbox and environment management with SSH key generation and SFTP support --- environment_test.go | 1 + go.mod | 5 +++ go.sum | 54 +++++++++++++++++++++++ lib/environment.go | 42 ++++++++++-------- lib/sandbox.go | 104 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 188 insertions(+), 18 deletions(-) diff --git a/environment_test.go b/environment_test.go index d68609b..3aea5f2 100644 --- a/environment_test.go +++ b/environment_test.go @@ -123,6 +123,7 @@ func TestEnvironmentUpdate(t *testing.T) { ) _, err := client.Environments.Update(context.TODO(), gitpod.EnvironmentUpdateParams{ Body: gitpod.EnvironmentUpdateParamsBodyMetadata{ + Metadata: gitpod.F[any](map[string]interface{}{}), }, }) diff --git a/go.mod b/go.mod index 1b30fb9..11c09b4 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,15 @@ module github.com/stainless-sdks/gitpod-go go 1.21 +require github.com/pkg/sftp v1.13.7 + require ( github.com/google/uuid v1.3.0 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 569e555..2ba67d2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= +github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -10,3 +21,46 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/environment.go b/lib/environment.go index e658d1e..ee309cc 100644 --- a/lib/environment.go +++ b/lib/environment.go @@ -38,7 +38,9 @@ func (s *environmentService) Create(ctx context.Context, options *CreateOptions) if err != nil { return nil, err } - return s.waitForRunning(ctx, envID) + return waitForRunning(ctx, s.Client, envID, func(env *gitpod.EnvironmentGetResponseEnvironment) (bool, error) { + return true, nil + }) } func (s *environmentService) Start(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { @@ -48,7 +50,9 @@ func (s *environmentService) Start(ctx context.Context, environmentID string) (* if err != nil { return nil, err } - return s.waitForRunning(ctx, environmentID) + return waitForRunning(ctx, s.Client, environmentID, func(env *gitpod.EnvironmentGetResponseEnvironment) (bool, error) { + return true, nil + }) } func (s *environmentService) Stop(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { @@ -58,7 +62,7 @@ func (s *environmentService) Stop(ctx context.Context, environmentID string) (*g if err != nil { return nil, err } - return s.waitForStopped(ctx, environmentID) + return waitForStopped(ctx, s.Client, environmentID) } func (s *environmentService) Delete(ctx context.Context, environmentID string) error { @@ -72,12 +76,12 @@ func (s *environmentService) Delete(ctx context.Context, environmentID string) e } return err } - return s.waitForDeleted(ctx, environmentID) + return waitForDeleted(ctx, s.Client, environmentID) } -func (s *environmentService) waitForDeleted(ctx context.Context, envID string) error { - _, err := s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { - resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ +func waitForDeleted(ctx context.Context, client *gitpod.Client, envID string) error { + _, err := waitForEnvironment(ctx, client, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { + resp, err := client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), }) if err != nil { @@ -92,9 +96,9 @@ func (s *environmentService) waitForDeleted(ctx context.Context, envID string) e return err } -func (s *environmentService) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { - return s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { - resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ +func waitForStopped(ctx context.Context, client *gitpod.Client, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return waitForEnvironment(ctx, client, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { + resp, err := client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), }) if err != nil { @@ -109,9 +113,9 @@ func (s *environmentService) waitForStopped(ctx context.Context, envID string) ( }) } -func (s *environmentService) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) { - return s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { - resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ +func waitForRunning(ctx context.Context, client *gitpod.Client, envID string, check func(*gitpod.EnvironmentGetResponseEnvironment) (bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return waitForEnvironment(ctx, client, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) { + resp, err := client.Environments.Get(ctx, gitpod.EnvironmentGetParams{ EnvironmentID: gitpod.String(envID), }) if err != nil { @@ -122,7 +126,11 @@ func (s *environmentService) waitForRunning(ctx context.Context, envID string) ( return nil, false, fmt.Errorf("environment creation failed: %s", fm) } if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseRunning { - return &resp.Environment, true, nil + ok, err := check(&resp.Environment) + if err != nil { + return nil, false, err + } + return &resp.Environment, ok, nil } if resp.Environment.Status.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusPhaseEnvironmentPhaseStopping { return nil, false, errors.New("environment creation failed: environment is stopping") @@ -140,11 +148,11 @@ func (s *environmentService) waitForRunning(ctx context.Context, envID string) ( }) } -func (s *environmentService) waitForEnvironment(ctx context.Context, envID string, check func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { - return waitFor(ctx, s.Client, envID, gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment, envID, check) +func waitForEnvironment(ctx context.Context, client *gitpod.Client, envID string, check func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) { + return WaitFor(ctx, client, envID, gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment, envID, check) } -func waitFor[T any](ctx context.Context, client *gitpod.Client, envID string, resourceType gitpod.EventWatchResponseResourceType, resourceID string, check func() (*T, bool, error)) (*T, error) { +func WaitFor[T any](ctx context.Context, client *gitpod.Client, envID string, resourceType gitpod.EventWatchResponseResourceType, resourceID string, check func() (*T, bool, error)) (*T, error) { env, ok, err := check() if err != nil { return nil, err diff --git a/lib/sandbox.go b/lib/sandbox.go index de2639c..157751e 100644 --- a/lib/sandbox.go +++ b/lib/sandbox.go @@ -2,12 +2,19 @@ package lib import ( "context" + "crypto/ed25519" "fmt" "io" "math/rand/v2" "net/http" + "net/url" "github.com/stainless-sdks/gitpod-go" + + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + + cryptorand "crypto/rand" ) type Sandbox interface { @@ -36,7 +43,21 @@ func NewSandbox(ctx context.Context, client *gitpod.Client, environmentID string return nil, nil } + /*signer, err := generateSSHKey() + if err != nil { + return nil, err + } + _, err = client.Environments.Update(ctx, gitpod.EnvironmentUpdateParams{ + Body: gitpod.EnvironmentUpdateParamsBodySpec{ + Spec: , + }, + }) + if err != nil { + return nil, err + }*/ + return &sandbox{ + taskReference: taskReference, taskID: resp.Task.ID, environmentID: environmentID, logAccessToken: logTokenResp.AccessToken, @@ -45,10 +66,12 @@ func NewSandbox(ctx context.Context, client *gitpod.Client, environmentID string } type sandbox struct { + taskReference string taskID string environmentID string logAccessToken string client *gitpod.Client + sshSigner ssh.Signer } func (s *sandbox) Close() error { @@ -75,7 +98,7 @@ func (s *sandbox) Exec(ctx context.Context, command string) (io.ReadCloser, erro return nil, err } taskExecutionID := startResp.TaskExecution.ID - taskExecution, err := waitFor(ctx, s.client, s.environmentID, gitpod.EventWatchResponseResourceTypeResourceTypeTaskExecution, taskExecutionID, func() (*gitpod.EnvironmentAutomationTaskExecutionGetResponseTaskExecution, bool, error) { + taskExecution, err := WaitFor(ctx, s.client, s.environmentID, gitpod.EventWatchResponseResourceTypeResourceTypeTaskExecution, taskExecutionID, func() (*gitpod.EnvironmentAutomationTaskExecutionGetResponseTaskExecution, bool, error) { resp, err := s.client.Environments.Automations.Tasks.Executions.Get(ctx, gitpod.EnvironmentAutomationTaskExecutionGetParams{ ID: gitpod.String(taskExecutionID), }) @@ -103,3 +126,82 @@ func (s *sandbox) Exec(ctx context.Context, command string) (io.ReadCloser, erro } return logResp.Body, nil } + +type FS struct { + *sftp.Client + sshConn *ssh.Client +} + +func (fs *FS) Close() error { + return fs.Client.Close() +} + +func (s *sandbox) FS(ctx context.Context) (*FS, error) { + env, err := waitForRunning(ctx, s.client, s.environmentID, func(env *gitpod.EnvironmentGetResponseEnvironment) (bool, error) { + keySpecified := false + for _, key := range env.Spec.SSHPublicKeys { + if key.ID == s.taskReference { + keySpecified = true + break + } + } + if !keySpecified { + return false, fmt.Errorf("public key not specified") + } + keyApplied := false + for _, key := range env.Status.SSHPublicKeys { + if key.ID == s.taskReference { + if key.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusSSHPublicKeysPhaseContentPhaseFailed { + return false, fmt.Errorf("public key failed to apply") + } + if key.Phase == gitpod.EnvironmentGetResponseEnvironmentStatusSSHPublicKeysPhaseContentPhaseReady { + keyApplied = true + break + } + } + } + return keyApplied && env.Status.EnvironmentURLs.SSH.URL == "", nil + }) + if err != nil { + return nil, err + } + sshURL := env.Status.EnvironmentURLs.SSH.URL + parsedURL, err := url.Parse(sshURL) + if err != nil { + return nil, err + } + + cfg := ssh.ClientConfig{ + User: "gitpod_devcontainer", + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(s.sshSigner), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + clnt, err := ssh.Dial("tcp", parsedURL.Host, &cfg) + if err != nil { + return nil, err + } + fs, err := sftp.NewClient(clnt) + if err != nil { + return nil, err + } + return &FS{ + Client: fs, + sshConn: clnt, + }, nil +} + +func generateSSHKey() (privateKeyPEM ssh.Signer, err error) { + _, privateKey, err := ed25519.GenerateKey(cryptorand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate key pair: %v", err) + } + + signer, err := ssh.NewSignerFromKey(privateKey) + if err != nil { + return nil, fmt.Errorf("failed to create signer from private key: %v", err) + } + + return signer, nil +} From 75350b1748097a3ee1899e014002ad4d3619cc00 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Thu, 6 Feb 2025 16:39:56 +0000 Subject: [PATCH 6/6] feat(api): add FS method to Sandbox interface for filesystem access --- lib/sandbox.go | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/sandbox.go b/lib/sandbox.go index 157751e..a092339 100644 --- a/lib/sandbox.go +++ b/lib/sandbox.go @@ -20,6 +20,7 @@ import ( type Sandbox interface { io.Closer Exec(ctx context.Context, command string) (io.ReadCloser, error) + FS(ctx context.Context) (*FS, error) } func NewSandbox(ctx context.Context, client *gitpod.Client, environmentID string) (Sandbox, error) {