diff --git a/packages/api/internal/handlers/sandbox_get.go b/packages/api/internal/handlers/sandbox_get.go index a029abc2a5..8370c90eff 100644 --- a/packages/api/internal/handlers/sandbox_get.go +++ b/packages/api/internal/handlers/sandbox_get.go @@ -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 @@ -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 @@ -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 diff --git a/packages/api/internal/handlers/sandbox_get_test.go b/packages/api/internal/handlers/sandbox_get_test.go new file mode 100644 index 0000000000..2992e30f79 --- /dev/null +++ b/packages/api/internal/handlers/sandbox_get_test.go @@ -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) +}