Skip to content

Commit 2af77d0

Browse files
committed
feat(api): implement sandbox management with execution and logging capabilities
1 parent 09b2691 commit 2af77d0

File tree

3 files changed

+145
-70
lines changed

3 files changed

+145
-70
lines changed
Lines changed: 40 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sandbox
1+
package lib
22

33
import (
44
"context"
@@ -10,87 +10,60 @@ import (
1010
"github.com/stainless-sdks/gitpod-go/internal/apierror"
1111
)
1212

13-
type CreateEnvironmentOptions struct {
13+
type CreateOptions struct {
1414
ProjectID string
1515
ContextURL string
1616
EnvironmentClass string
1717
}
1818

19-
type StartEnvironmentOptions struct {
20-
EnvironmentID string
19+
type EnvironmentService interface {
20+
Create(ctx context.Context, options *CreateOptions) (*gitpod.EnvironmentGetResponseEnvironment, error)
21+
Start(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error)
22+
Stop(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error)
23+
Delete(ctx context.Context, environmentID string) error
2124
}
2225

23-
type StopEnvironmentOptions struct {
24-
EnvironmentID string
25-
}
26-
27-
type DeleteEnvironmentOptions struct {
28-
EnvironmentID string
29-
}
30-
31-
type EnvironmentSandbox interface {
32-
// Get fetches an existing environment.
33-
Get(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error)
34-
// Create creates a new environment and waits for it to be running.
35-
Create(ctx context.Context, options *CreateEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error)
36-
// Start starts an existing environment and waits for it to be running.
37-
Start(ctx context.Context, options *StartEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error)
38-
// Stop stops an existing environment and waits for it to be stopped.
39-
Stop(ctx context.Context, options *StopEnvironmentOptions) (*gitpod.EnvironmentGetResponseEnvironment, error)
40-
// Delete deletes an existing environment and waits for it to be deleted.
41-
Delete(ctx context.Context, options *DeleteEnvironmentOptions) error
42-
}
43-
44-
type environmentSandbox struct {
45-
Client *gitpod.Client
46-
}
47-
48-
func NewEnvironmentSandbox(client *gitpod.Client) EnvironmentSandbox {
49-
return &environmentSandbox{
26+
func NewEnvironmentService(client *gitpod.Client) EnvironmentService {
27+
return &environmentService{
5028
Client: client,
5129
}
5230
}
5331

54-
func (s *environmentSandbox) Get(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
55-
resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{
56-
EnvironmentID: gitpod.String(envID),
57-
})
58-
if err != nil {
59-
return nil, err
60-
}
61-
return &resp.Environment, nil
32+
type environmentService struct {
33+
Client *gitpod.Client
6234
}
6335

64-
func (s *environmentSandbox) Create(ctx context.Context, options *CreateEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) {
36+
func (s *environmentService) Create(ctx context.Context, options *CreateOptions) (*gitpod.EnvironmentGetResponseEnvironment, error) {
6537
envID, err := s.create(ctx, options)
6638
if err != nil {
6739
return nil, err
6840
}
6941
return s.waitForRunning(ctx, envID)
7042
}
7143

72-
func (s *environmentSandbox) Start(ctx context.Context, options *StartEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) {
73-
_, err = s.Client.Environments.Start(ctx, gitpod.EnvironmentStartParams{
74-
EnvironmentID: gitpod.String(options.EnvironmentID),
44+
func (s *environmentService) Start(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
45+
_, err := s.Client.Environments.Start(ctx, gitpod.EnvironmentStartParams{
46+
EnvironmentID: gitpod.String(environmentID),
7547
})
7648
if err != nil {
7749
return nil, err
7850
}
79-
return s.waitForRunning(ctx, options.EnvironmentID)
51+
return s.waitForRunning(ctx, environmentID)
8052
}
8153

82-
func (s *environmentSandbox) Stop(ctx context.Context, options *StopEnvironmentOptions) (res *gitpod.EnvironmentGetResponseEnvironment, err error) {
83-
_, err = s.Client.Environments.Stop(ctx, gitpod.EnvironmentStopParams{
84-
EnvironmentID: gitpod.String(options.EnvironmentID),
54+
func (s *environmentService) Stop(ctx context.Context, environmentID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
55+
_, err := s.Client.Environments.Stop(ctx, gitpod.EnvironmentStopParams{
56+
EnvironmentID: gitpod.String(environmentID),
8557
})
8658
if err != nil {
8759
return nil, err
8860
}
89-
return s.waitForStopped(ctx, options.EnvironmentID)
61+
return s.waitForStopped(ctx, environmentID)
9062
}
91-
func (s *environmentSandbox) Delete(ctx context.Context, options *DeleteEnvironmentOptions) error {
63+
64+
func (s *environmentService) Delete(ctx context.Context, environmentID string) error {
9265
_, err := s.Client.Environments.Delete(ctx, gitpod.EnvironmentDeleteParams{
93-
EnvironmentID: gitpod.String(options.EnvironmentID),
66+
EnvironmentID: gitpod.String(environmentID),
9467
})
9568
if err != nil {
9669
apierr, ok := err.(*apierror.Error)
@@ -99,11 +72,11 @@ func (s *environmentSandbox) Delete(ctx context.Context, options *DeleteEnvironm
9972
}
10073
return err
10174
}
102-
return s.waitForDeleted(ctx, options.EnvironmentID)
75+
return s.waitForDeleted(ctx, environmentID)
10376
}
10477

105-
func (s *environmentSandbox) waitForDeleted(ctx context.Context, envID string) error {
106-
_, err := s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) {
78+
func (s *environmentService) waitForDeleted(ctx context.Context, envID string) error {
79+
_, err := s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) {
10780
resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{
10881
EnvironmentID: gitpod.String(envID),
10982
})
@@ -119,8 +92,8 @@ func (s *environmentSandbox) waitForDeleted(ctx context.Context, envID string) e
11992
return err
12093
}
12194

122-
func (s *environmentSandbox) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
123-
return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) {
95+
func (s *environmentService) waitForStopped(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
96+
return s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) {
12497
resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{
12598
EnvironmentID: gitpod.String(envID),
12699
})
@@ -136,8 +109,8 @@ func (s *environmentSandbox) waitForStopped(ctx context.Context, envID string) (
136109
})
137110
}
138111

139-
func (s *environmentSandbox) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
140-
return s.waitFor(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) {
112+
func (s *environmentService) waitForRunning(ctx context.Context, envID string) (*gitpod.EnvironmentGetResponseEnvironment, error) {
113+
return s.waitForEnvironment(ctx, envID, func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error) {
141114
resp, err := s.Client.Environments.Get(ctx, gitpod.EnvironmentGetParams{
142115
EnvironmentID: gitpod.String(envID),
143116
})
@@ -167,16 +140,20 @@ func (s *environmentSandbox) waitForRunning(ctx context.Context, envID string) (
167140
})
168141
}
169142

170-
func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) {
171-
env, ok, err := fetch()
143+
func (s *environmentService) waitForEnvironment(ctx context.Context, envID string, check func() (*gitpod.EnvironmentGetResponseEnvironment, bool, error)) (*gitpod.EnvironmentGetResponseEnvironment, error) {
144+
return waitFor(ctx, s.Client, envID, gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment, envID, check)
145+
}
146+
147+
func waitFor[T any](ctx context.Context, client *gitpod.Client, envID string, resourceType gitpod.EventWatchResponseResourceType, resourceID string, check func() (*T, bool, error)) (*T, error) {
148+
env, ok, err := check()
172149
if err != nil {
173150
return nil, err
174151
}
175152
if ok {
176153
return env, nil
177154
}
178155

179-
stream := s.Client.Events.WatchStreaming(ctx, gitpod.EventWatchParams{
156+
stream := client.Events.WatchStreaming(ctx, gitpod.EventWatchParams{
180157
Body: gitpod.EventWatchParamsBodyEnvironmentScopeProducesEventsForTheEnvironmentItselfAllTasksTaskExecutionsAndServicesAssociatedWithThatEnvironment{
181158
EnvironmentID: gitpod.String(envID),
182159
},
@@ -185,8 +162,8 @@ func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch fu
185162

186163
for stream.Next() {
187164
resp := stream.Current()
188-
if resp.ResourceType == gitpod.EventWatchResponseResourceTypeResourceTypeEnvironment && resp.ResourceID == envID {
189-
env, ok, err := fetch()
165+
if resp.ResourceType == resourceType && resp.ResourceID == resourceID {
166+
env, ok, err := check()
190167
if err != nil {
191168
// TODO: if transient we should retry?
192169
return nil, err
@@ -200,7 +177,7 @@ func (s *environmentSandbox) waitFor(ctx context.Context, envID string, fetch fu
200177
return nil, stream.Err()
201178
}
202179

203-
func (s *environmentSandbox) create(ctx context.Context, options *CreateEnvironmentOptions) (string, error) {
180+
func (s *environmentService) create(ctx context.Context, options *CreateOptions) (string, error) {
204181
if options.ProjectID != "" {
205182
resp, err := s.Client.Environments.NewFromProject(ctx, gitpod.EnvironmentNewFromProjectParams{
206183
ProjectID: gitpod.String(options.ProjectID),

lib/sandbox.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package lib
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"math/rand/v2"
8+
"net/http"
9+
10+
"github.com/stainless-sdks/gitpod-go"
11+
)
12+
13+
type Sandbox interface {
14+
io.Closer
15+
Exec(ctx context.Context, command string) (io.ReadCloser, error)
16+
}
17+
18+
func NewSandbox(ctx context.Context, client *gitpod.Client, environmentID string) (Sandbox, error) {
19+
logTokenResp, err := client.Environments.NewLogsToken(ctx, gitpod.EnvironmentNewLogsTokenParams{
20+
EnvironmentID: gitpod.String(environmentID),
21+
})
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
taskReference := fmt.Sprintf("sandbox-%d", rand.Uint64())
27+
resp, err := client.Environments.Automations.Tasks.New(ctx, gitpod.EnvironmentAutomationTaskNewParams{
28+
EnvironmentID: gitpod.String(environmentID),
29+
Metadata: gitpod.F(gitpod.EnvironmentAutomationTaskNewParamsMetadata{
30+
Reference: gitpod.String(taskReference),
31+
Name: gitpod.String("Sandbox Task"),
32+
Description: gitpod.String("Sandbox task"),
33+
}),
34+
})
35+
if err != nil {
36+
return nil, nil
37+
}
38+
39+
return &sandbox{
40+
taskID: resp.Task.ID,
41+
environmentID: environmentID,
42+
logAccessToken: logTokenResp.AccessToken,
43+
client: client,
44+
}, nil
45+
}
46+
47+
type sandbox struct {
48+
taskID string
49+
environmentID string
50+
logAccessToken string
51+
client *gitpod.Client
52+
}
53+
54+
func (s *sandbox) Close() error {
55+
_, err := s.client.Environments.Automations.Tasks.Delete(context.Background(), gitpod.EnvironmentAutomationTaskDeleteParams{
56+
ID: gitpod.String(s.taskID),
57+
})
58+
return err
59+
}
60+
61+
func (s *sandbox) Exec(ctx context.Context, command string) (io.ReadCloser, error) {
62+
_, err := s.client.Environments.Automations.Tasks.Update(ctx, gitpod.EnvironmentAutomationTaskUpdateParams{
63+
ID: gitpod.String(s.taskID),
64+
Spec: gitpod.F[gitpod.EnvironmentAutomationTaskUpdateParamsSpecUnion](gitpod.EnvironmentAutomationTaskUpdateParamsSpecCommand{
65+
Command: gitpod.String(command),
66+
}),
67+
})
68+
if err != nil {
69+
return nil, err
70+
}
71+
startResp, err := s.client.Environments.Automations.Tasks.Start(ctx, gitpod.EnvironmentAutomationTaskStartParams{
72+
ID: gitpod.String(s.taskID),
73+
})
74+
if err != nil {
75+
return nil, err
76+
}
77+
taskExecutionID := startResp.TaskExecution.ID
78+
taskExecution, err := waitFor(ctx, s.client, s.environmentID, gitpod.EventWatchResponseResourceTypeResourceTypeTaskExecution, taskExecutionID, func() (*gitpod.EnvironmentAutomationTaskExecutionGetResponseTaskExecution, bool, error) {
79+
resp, err := s.client.Environments.Automations.Tasks.Executions.Get(ctx, gitpod.EnvironmentAutomationTaskExecutionGetParams{
80+
ID: gitpod.String(taskExecutionID),
81+
})
82+
if err != nil {
83+
return nil, false, err
84+
}
85+
if resp.TaskExecution.Status.LogURL != "" {
86+
return &resp.TaskExecution, true, nil
87+
}
88+
return nil, false, nil
89+
})
90+
if err != nil {
91+
return nil, err
92+
}
93+
logURL := taskExecution.Status.LogURL
94+
95+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, logURL, nil)
96+
if err != nil {
97+
return nil, err
98+
}
99+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.logAccessToken))
100+
logResp, err := http.DefaultClient.Do(req)
101+
if err != nil {
102+
return nil, err
103+
}
104+
return logResp.Body, nil
105+
}

lib/sandbox/sandbox_test.go

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)