diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7d665dc01f..cf26529478 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -39,6 +39,8 @@ flowchart TB LB["Load balancer
api.* | *.domain wildcard | docker.*"] + VC["volume-content API (belt)
api.<domain>"] + subgraph controlplane["Control plane (API node pool)"] API["API
REST :80, gRPC :5009/:5109"] DashAPI["dashboard-api :3010"] @@ -68,6 +70,8 @@ flowchart TB SDK -->|REST| LB --> API Browser -->|"port-sandboxid.domain"| LB --> CP Docker --> LB --> DRP + API -.->|"mint content token + domain"| SDK + SDK -->|"volume content (token-authed)
api.<BYOC or default domain>"| VC API -->|"gRPC Create/Delete/Pause"| ORCH API -->|"gRPC TemplateCreate"| TM CP -->|"lookup sandbox → node"| RD @@ -306,6 +310,43 @@ sequenceDiagram E-->>U: response ``` +### Volume content + +Persistent volumes (`packages/orchestrator/pkg/volumes/`) are managed through the control-plane +API (`POST/GET /volumes`), but their **content** — reading and writing files — is served by a +separate volume-content API (belt, `e2b-dev/belt`) that the SDK talks to directly, not through the +control-plane API. The API's role is to mint the credential and tell the SDK where to send content +traffic. + +```mermaid +sequenceDiagram + autonumber + participant U as SDK + participant API as API + participant PG as PostgreSQL + participant VC as volume-content API (belt) + + U->>API: POST /volumes (create) or GET /volumes/{id} + API->>PG: persist / load volume row + API->>API: mint JWT (aud = https://api.<domain>)
resolve domain + API-->>U: { volumeID, name, token, domain? } + Note over U: domain is returned only for BYOC teams;
SDK stores it and falls back to api.<E2B_DOMAIN> otherwise + U->>VC: /volumecontent/{id}/... at api.<domain>
Authorization: Bearer token + VC->>VC: verify token (audience must match its own origin) + VC-->>U: file content +``` + +- **Domain selection.** The token's audience and the content host are the same origin, + `https://api.`. For teams on a **custom (BYOC) cluster** (`team.ClusterID` set), the API + returns that cluster's domain (`cluster.SandboxDomain`, resolved in + `handlers.volumeContentDomain`) so content traffic goes to the BYOC cluster's edge instead of the + control-plane host. For teams on the default cluster the response omits `domain` and the SDK uses + its configured default (`api.`); the audience then uses the deployment's `DOMAIN_NAME`. +- **Token.** A short-lived JWT (`handlers.generateVolumeContentToken`, config in + `cfg.VolumesTokenConfig`) signed by the API, scoped to the team and volume, presented as a bearer + token on every content request. Its `aud` claim is `https://api.`, so a token minted for + one cluster's origin is not accepted by another. + ### Pause and resume - **Pause**: API records a snapshot row in Postgres, then gRPC `Pause` to the node. The diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index 7d7b9b83e0..be2b029a79 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -1481,6 +1481,12 @@ type Volume struct { // VolumeAndToken defines model for VolumeAndToken. type VolumeAndToken struct { + // Domain Domain to use as the destination for volume content requests, + // replacing the default `api.`. Only returned when the + // team is connected to a custom (BYOC) cluster; absent otherwise, in + // which case the default domain is used. + Domain *string `json:"domain,omitempty"` + // Name Name of the volume Name string `json:"name"` @@ -14037,210 +14043,212 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "/i4Ens83/xsAAP//", + "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", + "uxB4Pt/+vwAAAP//", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/api/internal/clusters/mock.go b/packages/api/internal/clusters/mock.go new file mode 100644 index 0000000000..375ee6a244 --- /dev/null +++ b/packages/api/internal/clusters/mock.go @@ -0,0 +1,29 @@ +package clusters + +import ( + "github.com/google/uuid" + + "github.com/e2b-dev/infra/packages/shared/pkg/smap" +) + +// NewTestPool builds a Pool pre-populated with the given clusters, for use in +// tests that need cluster lookups (e.g. GetClusterById) without spinning up a +// full synchronization loop. +func NewTestPool(clusters ...*Cluster) *Pool { + m := smap.New[*Cluster]() + for _, c := range clusters { + m.Insert(c.ID.String(), c) + } + + return &Pool{clusters: m} +} + +// NewTestCluster builds a minimal Cluster carrying just an ID and sandbox +// domain, for tests. Other collaborators (instances, synchronization, +// resources) are left nil. +func NewTestCluster(id uuid.UUID, sandboxDomain *string) *Cluster { + return &Cluster{ + ID: id, + SandboxDomain: sandboxDomain, + } +} diff --git a/packages/api/internal/handlers/volume_create.go b/packages/api/internal/handlers/volume_create.go index 200080ba64..e202e4c050 100644 --- a/packages/api/internal/handlers/volume_create.go +++ b/packages/api/internal/handlers/volume_create.go @@ -89,6 +89,16 @@ func (a *APIStore) PostVolumes(c *gin.Context) { clusterID := clustershared.WithClusterFallback(team.ClusterID) + // Resolve the BYOC domain up front so we fail before allocating any + // resources if the team's cluster can't be found. + domain, err := a.volumeContentDomain(team) + if err != nil { + a.sendAPIStoreError(c, http.StatusServiceUnavailable, "Cluster not found") + telemetry.ReportError(ctx, "cluster not found", err) + + return + } + // The volume identity we intend to create. We generate the ID up front so we // can hand it to the orchestrator, which is given the chance to adjust these // values (e.g. resolve a placeholder volume type) and returns the definitive @@ -164,7 +174,7 @@ func (a *APIStore) PostVolumes(c *gin.Context) { Set("volume_type", volume.VolumeType), ) - token, apiErr := generateVolumeContentToken(a.config.VolumesToken, volume, team) + token, apiErr := generateVolumeContentToken(a.config.VolumesToken, volume, team, a.volumeTokenAudience(domain)) if apiErr != nil { a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) telemetry.ReportCriticalError(ctx, apiErr.ClientMsg, apiErr.Err) @@ -176,6 +186,7 @@ func (a *APIStore) PostVolumes(c *gin.Context) { VolumeID: volume.ID.String(), Name: volume.Name, Token: token, + Domain: domain, } c.JSON(http.StatusCreated, result) diff --git a/packages/api/internal/handlers/volume_get.go b/packages/api/internal/handlers/volume_get.go index 01af912c8c..f750975aac 100644 --- a/packages/api/internal/handlers/volume_get.go +++ b/packages/api/internal/handlers/volume_get.go @@ -15,7 +15,15 @@ func (a *APIStore) GetVolumesVolumeID(c *gin.Context, volumeID api.VolumeID) { return } - token, err := generateVolumeContentToken(a.config.VolumesToken, volume, team) + domain, domainErr := a.volumeContentDomain(team) + if domainErr != nil { + a.sendAPIStoreError(c, http.StatusServiceUnavailable, "Cluster not found") + telemetry.ReportError(c.Request.Context(), "cluster not found", domainErr) + + return + } + + token, err := generateVolumeContentToken(a.config.VolumesToken, volume, team, a.volumeTokenAudience(domain)) if err != nil { a.sendAPIStoreError(c, http.StatusInternalServerError, "failed to sign token") telemetry.ReportCriticalError(c.Request.Context(), "failed to sign token", err) @@ -27,6 +35,7 @@ func (a *APIStore) GetVolumesVolumeID(c *gin.Context, volumeID api.VolumeID) { VolumeID: volume.ID.String(), Name: volume.Name, Token: token, + Domain: domain, } c.JSON(http.StatusOK, result) diff --git a/packages/api/internal/handlers/volume_get_test.go b/packages/api/internal/handlers/volume_get_test.go index ff1a6b561c..898256824d 100644 --- a/packages/api/internal/handlers/volume_get_test.go +++ b/packages/api/internal/handlers/volume_get_test.go @@ -45,7 +45,8 @@ func TestGenerateVolumeContentToken_SetsTokidHeader(t *testing.T) { } // Act: generate token - tokenStr, apiErr := generateVolumeContentToken(config, volume, team) + audience := "https://api.custom.example.com" + tokenStr, apiErr := generateVolumeContentToken(config, volume, team, audience) require.Nil(t, apiErr) require.NotEmpty(t, tokenStr) @@ -64,4 +65,11 @@ func TestGenerateVolumeContentToken_SetsTokidHeader(t *testing.T) { tokidVal, ok := parsed.Header["tokid"].(string) require.True(t, ok, "expected custom header 'tokid' to be present and a string") require.Equal(t, config.SigningKeyName, tokidVal) + + // Assert: the audience claim is the origin we passed, not the cluster ID. + claims, ok := parsed.Claims.(jwt.MapClaims) + require.True(t, ok) + aud, err := claims.GetAudience() + require.NoError(t, err) + require.Equal(t, jwt.ClaimStrings{audience}, aud) } diff --git a/packages/api/internal/handlers/volume_token.go b/packages/api/internal/handlers/volume_token.go index e5e84b04d3..2a0ed978d2 100644 --- a/packages/api/internal/handlers/volume_token.go +++ b/packages/api/internal/handlers/volume_token.go @@ -13,12 +13,14 @@ import ( "github.com/e2b-dev/infra/packages/api/internal/cfg" "github.com/e2b-dev/infra/packages/auth/pkg/types" "github.com/e2b-dev/infra/packages/db/queries" - "github.com/e2b-dev/infra/packages/shared/pkg/clusters" ) var ErrVolumesTokenNotConfigured = errors.New("volumes content token signing is not supported by this deployment") -func generateVolumeContentToken(config cfg.VolumesTokenConfig, volume queries.Volume, team *types.Team) (string, *api.APIError) { +// generateVolumeContentToken mints the JWT the SDK presents to the volume +// content API. audience is the origin the token is valid for +// (`https://api.`, see volumeTokenAudience). +func generateVolumeContentToken(config cfg.VolumesTokenConfig, volume queries.Volume, team *types.Team, audience string) (string, *api.APIError) { if !config.IsConfigured() { return "", &api.APIError{ Err: ErrVolumesTokenNotConfigured, @@ -27,14 +29,12 @@ func generateVolumeContentToken(config cfg.VolumesTokenConfig, volume queries.Vo } } - clusterID := clusters.WithClusterFallback(team.ClusterID) - now := time.Now() expiration := now.Add(config.Duration) claims := jwt.MapClaims{ // registered - "aud": clusterID.String(), + "aud": audience, "exp": jwt.NewNumericDate(expiration), "iat": jwt.NewNumericDate(now), "iss": config.Issuer, diff --git a/packages/api/internal/handlers/volume_util.go b/packages/api/internal/handlers/volume_util.go index e26f281108..59cd0aca4f 100644 --- a/packages/api/internal/handlers/volume_util.go +++ b/packages/api/internal/handlers/volume_util.go @@ -80,6 +80,38 @@ func (a *APIStore) getVolume(c *gin.Context, volumeID string) (queries.Volume, * var ErrClusterNotFound = errors.New("cluster not found") +// volumeContentDomain resolves the domain the SDK should use as the destination +// for volume content requests. Teams connected to a custom (BYOC) cluster get +// that cluster's domain, which the SDK uses in place of the default +// api. host. Teams on the default cluster get nil, leaving the SDK +// on its configured default. +func (a *APIStore) volumeContentDomain(team *types.Team) (*string, error) { + if team.ClusterID == nil { + return nil, nil + } + + cluster, ok := a.clusters.GetClusterById(*team.ClusterID) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrClusterNotFound, team.ClusterID.String()) + } + + return cluster.SandboxDomain, nil +} + +// volumeTokenAudience builds the audience claim for a volume content token: the +// origin the SDK targets for content requests, i.e. `https://api.`. The +// domain is the team's BYOC cluster domain when set (as resolved by +// volumeContentDomain) and the deployment default otherwise, matching the host +// the token is actually presented to. +func (a *APIStore) volumeTokenAudience(domain *string) string { + host := a.config.DomainName + if domain != nil && *domain != "" { + host = *domain + } + + return fmt.Sprintf("https://api.%s", host) +} + var ErrNoHealthyOrchestratorFound = errors.New("no healthy orchestrator found") var ErrUnknownVolumeType = errors.New("unknown volume type") diff --git a/packages/api/internal/handlers/volume_util_test.go b/packages/api/internal/handlers/volume_util_test.go index 3ef5453f39..2acb3eeb09 100644 --- a/packages/api/internal/handlers/volume_util_test.go +++ b/packages/api/internal/handlers/volume_util_test.go @@ -3,12 +3,78 @@ package handlers import ( "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/e2b-dev/infra/packages/api/internal/api" + "github.com/e2b-dev/infra/packages/api/internal/cfg" + "github.com/e2b-dev/infra/packages/api/internal/clusters" "github.com/e2b-dev/infra/packages/api/internal/orchestrator/nodemanager" + "github.com/e2b-dev/infra/packages/auth/pkg/types" + authqueries "github.com/e2b-dev/infra/packages/db/pkg/auth/queries" ) +func TestVolumeTokenAudience(t *testing.T) { + t.Parallel() + + store := &APIStore{config: cfg.Config{DomainName: "e2b.app"}} + + t.Run("falls back to the deployment domain", func(t *testing.T) { + t.Parallel() + + assert.Equal(t, "https://api.e2b.app", store.volumeTokenAudience(nil)) + }) + + t.Run("uses the BYOC domain when set", func(t *testing.T) { + t.Parallel() + + domain := "custom.example.com" + assert.Equal(t, "https://api.custom.example.com", store.volumeTokenAudience(&domain)) + }) +} + +func TestVolumeContentDomain(t *testing.T) { + t.Parallel() + + clusterID := uuid.New() + domain := "custom.example.com" + + store := &APIStore{ + clusters: clusters.NewTestPool(clusters.NewTestCluster(clusterID, &domain)), + } + + teamWith := func(id *uuid.UUID) *types.Team { + return &types.Team{Team: &authqueries.Team{ID: uuid.New(), ClusterID: id}} + } + + t.Run("no cluster returns nil domain", func(t *testing.T) { + t.Parallel() + + got, err := store.volumeContentDomain(teamWith(nil)) + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("BYOC cluster returns its domain", func(t *testing.T) { + t.Parallel() + + got, err := store.volumeContentDomain(teamWith(&clusterID)) + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, domain, *got) + }) + + t.Run("unknown cluster returns error", func(t *testing.T) { + t.Parallel() + + unknown := uuid.New() + got, err := store.volumeContentDomain(teamWith(&unknown)) + require.ErrorIs(t, err, ErrClusterNotFound) + assert.Nil(t, got) + }) +} + func TestFindNodesByVolumeLabel(t *testing.T) { t.Parallel() diff --git a/spec/openapi.yml b/spec/openapi.yml index e3822f2dae..39a344ccd9 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -2058,6 +2058,13 @@ components: token: type: string description: Auth token to use for interacting with volume content + domain: + type: string + description: | + Domain to use as the destination for volume content requests, + replacing the default `api.`. Only returned when the + team is connected to a custom (BYOC) cluster; absent otherwise, in + which case the default domain is used. required: - volumeID - name diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index 9aea20f7af..924f97841c 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -1476,6 +1476,12 @@ type Volume struct { // VolumeAndToken defines model for VolumeAndToken. type VolumeAndToken struct { + // Domain Domain to use as the destination for volume content requests, + // replacing the default `api.`. Only returned when the + // team is connected to a custom (BYOC) cluster; absent otherwise, in + // which case the default domain is used. + Domain *string `json:"domain,omitempty"` + // Name Name of the volume Name string `json:"name"`