Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 56 additions & 49 deletions packages/api/internal/handlers/sandbox_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,58 +120,60 @@ func (a *APIStore) GetSandboxesSandboxID(c *gin.Context, id string) {
}

// Try to get the running sandbox first
sbx, err := a.orchestrator.GetSandbox(ctx, team.ID, sandboxId)
if err == nil {
// Check if sandbox belongs to the team
if sbx.TeamID != team.ID {
telemetry.ReportCriticalError(ctx, fmt.Sprintf("sandbox '%s' doesn't belong to team '%s'", sandboxId, team.ID.String()), nil)
a.sendAPIStoreError(c, http.StatusNotFound, utils.SandboxNotFoundMsg(id))
if a.orchestrator != nil {
sbx, err := a.orchestrator.GetSandbox(ctx, team.ID, sandboxId)
if err == nil {
// Check if sandbox belongs to the team
if sbx.TeamID != team.ID {
telemetry.ReportCriticalError(ctx, fmt.Sprintf("sandbox '%s' doesn't belong to team '%s'", sandboxId, team.ID.String()), nil)
a.sendAPIStoreError(c, http.StatusNotFound, utils.SandboxNotFoundMsg(id))

return
}

return
}
state := api.Running
switch sbx.State {
// Sandbox is being paused or already is paused, user can work with that as if it's paused
case sandbox.StatePausing:
state = api.Paused
// Sandbox is being stopped or already is stopped, user can't work with it anymore
case sandbox.StateKilling:
logger.L().Debug(ctx, "Sandbox is being killed", logger.WithSandboxID(sandboxId))
a.sendAPIStoreError(c, http.StatusNotFound, utils.SandboxNotFoundMsg(id))

return
}

state := api.Running
switch sbx.State {
// Sandbox is being paused or already is paused, user can work with that as if it's paused
case sandbox.StatePausing:
state = api.Paused
// Sandbox is being stopped or already is stopped, user can't work with it anymore
case sandbox.StateKilling:
logger.L().Debug(ctx, "Sandbox is being killed", logger.WithSandboxID(sandboxId))
a.sendAPIStoreError(c, http.StatusNotFound, utils.SandboxNotFoundMsg(id))
// Sandbox exists and belongs to the team - return running sandbox sbx
sandbox := api.SandboxDetail{
ClientID: sbx.ClientID,
TemplateID: sbx.BaseTemplateID,
Alias: sbx.Alias,
SandboxID: sbx.SandboxID,
StartedAt: sbx.StartTime,
CpuCount: api.CPUCount(sbx.VCpu),
MemoryMB: api.MemoryMB(sbx.RamMB),
DiskSizeMB: api.DiskSizeMB(sbx.TotalDiskSizeMB),
EndAt: sbx.EndTime,
State: state,
EnvdVersion: sbx.EnvdVersion,
EnvdAccessToken: sbx.EnvdAccessToken,
AllowInternetAccess: sbx.AllowInternetAccess,
Domain: sbxDomain,
Network: dbNetworkConfigToAPI(sbx.Network),
Lifecycle: sandboxLifecycleToAPI(sbx.AutoPause, sbx.AutoResume),
VolumeMounts: convertFromDBMountsToAPIMounts(sbx.VolumeMounts),
}

return
}
if sbx.Metadata != nil {
meta := api.SandboxMetadata(sbx.Metadata)
sandbox.Metadata = &meta
}

// Sandbox exists and belongs to the team - return running sandbox sbx
sandbox := api.SandboxDetail{
ClientID: sbx.ClientID,
TemplateID: sbx.BaseTemplateID,
Alias: sbx.Alias,
SandboxID: sbx.SandboxID,
StartedAt: sbx.StartTime,
CpuCount: api.CPUCount(sbx.VCpu),
MemoryMB: api.MemoryMB(sbx.RamMB),
DiskSizeMB: api.DiskSizeMB(sbx.TotalDiskSizeMB),
EndAt: sbx.EndTime,
State: state,
EnvdVersion: sbx.EnvdVersion,
EnvdAccessToken: sbx.EnvdAccessToken,
AllowInternetAccess: sbx.AllowInternetAccess,
Domain: sbxDomain,
Network: dbNetworkConfigToAPI(sbx.Network),
Lifecycle: sandboxLifecycleToAPI(sbx.AutoPause, sbx.AutoResume),
VolumeMounts: convertFromDBMountsToAPIMounts(sbx.VolumeMounts),
}
c.JSON(http.StatusOK, sandbox)

if sbx.Metadata != nil {
meta := api.SandboxMetadata(sbx.Metadata)
sandbox.Metadata = &meta
return
}

c.JSON(http.StatusOK, sandbox)

return
}

// If sandbox not found try to get the latest snapshot
Expand Down Expand Up @@ -232,12 +234,17 @@ func (a *APIStore) GetSandboxesSandboxID(c *gin.Context, id string) {

var autoResumeConfig *dbtypes.SandboxAutoResumeConfig
var networkConfig *dbtypes.SandboxNetworkConfig
var volumeMounts []*dbtypes.SandboxVolumeMountConfig
if lastSnapshot.Snapshot.Config != nil {
autoResumeConfig = lastSnapshot.Snapshot.Config.AutoResume
networkConfig = lastSnapshot.Snapshot.Config.Network
volumeMounts = lastSnapshot.Snapshot.Config.VolumeMounts
}

pausedAlias := firstAlias(lastSnapshot.Aliases)
var alias *string
if len(lastSnapshot.Aliases) > 0 {
alias = &lastSnapshot.Aliases[0]
}

sandbox := api.SandboxDetail{
ClientID: consts.ClientID, // for backwards compatibility we need to return a client id
Expand All @@ -255,10 +262,10 @@ func (a *APIStore) GetSandboxesSandboxID(c *gin.Context, id string) {
Domain: nil,
Network: dbNetworkConfigToAPI(networkConfig),
Lifecycle: sandboxLifecycleToAPI(lastSnapshot.Snapshot.AutoPause, autoResumeConfig),
VolumeMounts: convertFromDBMountsToAPIMounts(volumeMounts),
Alias: alias,
}

sandbox.Alias = &pausedAlias

if lastSnapshot.Snapshot.Metadata != nil {
metadata := api.SandboxMetadata(lastSnapshot.Snapshot.Metadata)
sandbox.Metadata = &metadata
Expand Down
114 changes: 114 additions & 0 deletions packages/api/internal/handlers/sandbox_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package handlers

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

apispec "github.com/e2b-dev/infra/packages/api/internal/api"
snapshotcache "github.com/e2b-dev/infra/packages/api/internal/cache/snapshots"
"github.com/e2b-dev/infra/packages/api/internal/sandbox"
"github.com/e2b-dev/infra/packages/auth/pkg/auth"
"github.com/e2b-dev/infra/packages/auth/pkg/types"
authqueries "github.com/e2b-dev/infra/packages/db/pkg/auth/queries"
"github.com/e2b-dev/infra/packages/db/pkg/testutils"
dbtypes "github.com/e2b-dev/infra/packages/db/pkg/types"
"github.com/e2b-dev/infra/packages/db/queries"
"github.com/e2b-dev/infra/packages/shared/pkg/id"
redis_utils "github.com/e2b-dev/infra/packages/shared/pkg/redis"
)

func TestGetSandboxesSandboxID_PausedWithVolumeMountsAndNilAlias(t *testing.T) {
t.Parallel()

testDB := testutils.SetupDatabase(t)
redis := redis_utils.SetupInstance(t)
ctx := t.Context()

teamID := testutils.CreateTestTeam(t, testDB)
teamSlug := testutils.GetTeamSlug(t, ctx, testDB, teamID)
baseTemplateID := testutils.CreateTestTemplate(t, testDB, teamID)
snapshotTemplateID := id.Generate()
sandboxID := id.Generate()

volID := uuid.New()
totalDiskSize := int64(1024)
envdVersion := "v1.0.0"
allowInternet := true

config := &dbtypes.PausedSandboxConfig{
VolumeMounts: []*dbtypes.SandboxVolumeMountConfig{
{
ID: volID.String(),
Name: "my-volume",
Path: "/mnt/data",
Type: "nfs",
},
},
}

_, err := testDB.SqlcClient.UpsertSnapshot(ctx, queries.UpsertSnapshotParams{
TemplateID: snapshotTemplateID,
TeamID: teamID,
SandboxID: sandboxID,
BaseTemplateID: baseTemplateID,
StartedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
Vcpu: 2,
RamMb: 2048,
TotalDiskSizeMb: &totalDiskSize,
Metadata: dbtypes.JSONBStringMap{},
KernelVersion: "6.1.0",
FirecrackerVersion: "1.4.0",
EnvdVersion: &envdVersion,
Secure: true,
AllowInternetAccess: &allowInternet,
AutoPause: true,
OriginNodeID: "test-node",
Status: dbtypes.BuildStatusSuccess,
Config: config,
})
require.NoError(t, err)

tokenGen, err := sandbox.NewAccessTokenGenerator("test-secret-at-least-32-chars-long!")
require.NoError(t, err)

store := &APIStore{
sqlcDB: testDB.SqlcClient,
authDB: testDB.AuthDb,
snapshotCache: snapshotcache.NewSnapshotCache(testDB.SqlcClient, redis),
accessTokenGenerator: tokenGen,
}

w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequestWithContext(ctx, http.MethodGet, "/sandboxes/"+sandboxID, nil)
auth.SetTeamInfoForTest(t, c, &types.Team{
Team: &authqueries.Team{
ID: teamID,
Slug: teamSlug,
},
})

store.GetSandboxesSandboxID(c, sandboxID)

require.Equal(t, http.StatusOK, w.Code)

var res apispec.SandboxDetail
err = json.Unmarshal(w.Body.Bytes(), &res)
require.NoError(t, err)

assert.Equal(t, apispec.Paused, res.State)
assert.Nil(t, res.Alias, "Alias should be nil when there are no aliases")
require.NotNil(t, res.VolumeMounts, "VolumeMounts should be populated for paused sandbox")
require.Len(t, *res.VolumeMounts, 1)
assert.Equal(t, "my-volume", (*res.VolumeMounts)[0].Name)
assert.Equal(t, "/mnt/data", (*res.VolumeMounts)[0].Path)
}