Skip to content

Commit e2abd1d

Browse files
authored
feat(orch): move reserved blocks setting from in-guest to host-side pre-boot hook (#2199)
* feat(orch): add PreBootFn hook to Factory.CreateSandbox for host-side rootfs modifications Add a PreBootFn callback parameter to Factory.CreateSandbox() that runs after the rootfs device is ready but before Firecracker boots the VM. This enables host-side filesystem metadata changes (e.g. tune2fs) on the rootfs path without depending on guest-side tooling. Thread the hook through the layer CreateSandbox builder via a new WithPreBootFn option. * feat(orch): move reserved blocks setting from in-guest to host-side pre-boot hook Replace SetReservedBlocksInGuest (tune2fs inside the VM) with a host-side PreBootFn that calls SetReservedBlocksOnHost on the rootfs device path before the guest kernel boots. This eliminates the dependency on e2fsprogs being installed and compatible inside the guest image. The previous in-guest approach was fragile across different build paths: - fromImage builds: provisioning could install e2fsprogs, but older guest versions were incompatible with host-created ext4 features. - fromTemplate builds: provisioning is skipped entirely, so e2fsprogs was only present if the parent template happened to have it. - Cached builds: the base phase SetReservedBlocksOnHost was skipped on cache hits, leaving only the in-guest call which could fail. Reserved blocks are now set at two points: - Base phase: on the host rootfs file after provisioning, so root has protected disk space during user build steps. - Finalize phase: via PreBootFn before every VM boot, regardless of build path or cache state, using the host's own tune2fs. * refactor(orch): collect finalize sandbox options into a slice Gather CreateSandbox options up front and spread them, rather than always passing WithPreBootFn which could be nil. * feat(orch): set reserved blocks on first uncached step layer via PreBootFn Apply the host-side SetReservedBlocksOnHost pre-boot hook to the steps phase as well, not just finalize. When the first uncached layer creates a new sandbox, the PreBootFn sets reserved blocks before the guest boots. This protects intermediate build sandboxes from disk-full conditions during user RUN commands, not just the final template. * refactor(orch): unify base phase reserved blocks to use PreBootFn hook Move the inline SetReservedBlocksOnHost call in buildLayerFromOCI to use the same WithPreBootFn mechanism as the steps and finalize phases. This is a pure refactor — same file, same timing (before VM boot), just consistent with the rest of the pipeline. * refactor(orch): extract ReservedBlocksOptions helper to deduplicate PreBootFn setup Consolidate the identical reserved blocks PreBootFn setup from base, steps, and finalize phases into a single ReservedBlocksOptions() helper in the layer package. Each call site now collapses from 5 lines to 1. * fix(orch): guard finalize ReservedBlocksOptions with sourceLayer.Cached Only set reserved blocks in finalize when the source layer was cached, since otherwise a prior step phase already set them via its own CreateSandbox PreBootFn.
1 parent b971717 commit e2abd1d

10 files changed

Lines changed: 80 additions & 53 deletions

File tree

packages/orchestrator/pkg/sandbox/sandbox.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,12 @@ func NewFactory(
295295
}
296296
}
297297

298+
// PreBootFn is an optional callback invoked after the rootfs is ready but before
299+
// Firecracker boots. It receives the rootfs device path (e.g., a file path for
300+
// DirectProvider or /dev/nbdX for NBDProvider) and may modify the filesystem
301+
// on the host side.
302+
type PreBootFn func(ctx context.Context, rootfsPath string) error
303+
298304
// CreateSandbox creates the sandbox.
299305
// IMPORTANT: You must Close() the sandbox after you are done with it.
300306
func (f *Factory) CreateSandbox(
@@ -306,6 +312,7 @@ func (f *Factory) CreateSandbox(
306312
rootfsCachePath string,
307313
processOptions fc.ProcessOptions,
308314
apiConfigToStore *orchestrator.SandboxConfig,
315+
preBootFn PreBootFn,
309316
) (s *Sandbox, e error) {
310317
ctx, span := tracer.Start(ctx, "create sandbox")
311318
defer span.End()
@@ -380,6 +387,19 @@ func (f *Factory) CreateSandbox(
380387
return nil, err
381388
}
382389

390+
// Run the optional pre-boot hook before Firecracker starts.
391+
// This allows host-side filesystem changes before the guest kernel takes charge.
392+
if preBootFn != nil {
393+
rootfsPath, pathErr := rootfsProvider.Path()
394+
if pathErr != nil {
395+
return nil, fmt.Errorf("failed to get rootfs path for pre-boot hook: %w", pathErr)
396+
}
397+
398+
if hookErr := preBootFn(ctx, rootfsPath); hookErr != nil {
399+
return nil, fmt.Errorf("pre-boot hook failed: %w", hookErr)
400+
}
401+
}
402+
383403
cgroupHandle, cgroupFD := createCgroup(ctx, f.cgroupManager, sandboxFiles.SandboxCgroupName(), cleanup)
384404
defer releaseCgroupFD(ctx, cgroupHandle, runtime.SandboxID)
385405

packages/orchestrator/pkg/template/build/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ func runBuild(
301301
commandExecutor,
302302
index,
303303
builder.metrics,
304+
builder.featureFlags,
304305
config.TemplateDefaultUser,
305306
bc.Config.Force,
306307
)
@@ -314,6 +315,7 @@ func runBuild(
314315
commandExecutor,
315316
index,
316317
builder.metrics,
318+
builder.featureFlags,
317319
)
318320

319321
postProcessingBuilder := finalize.New(

packages/orchestrator/pkg/template/build/layer/create_sandbox.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import (
1313
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/fc"
1414
sbxtemplate "github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox/template"
1515
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/config"
16+
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/core/filesystem"
1617
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/constants"
1718
"github.com/e2b-dev/infra/packages/orchestrator/pkg/units"
1819
"github.com/e2b-dev/infra/packages/shared/pkg/env"
1920
"github.com/e2b-dev/infra/packages/shared/pkg/fc/models"
21+
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
2022
"github.com/e2b-dev/infra/packages/shared/pkg/id"
2123
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
2224
)
@@ -29,6 +31,7 @@ type CreateSandbox struct {
2931

3032
rootfsCachePath string
3133
ioEngine *string
34+
preBootFn sandbox.PreBootFn
3235
}
3336

3437
const (
@@ -41,6 +44,7 @@ var _ SandboxCreator = (*CreateSandbox)(nil)
4144
type createSandboxOptions struct {
4245
rootfsCachePath string
4346
ioEngine *string
47+
preBootFn sandbox.PreBootFn
4448
}
4549

4650
type CreateSandboxOption func(*createSandboxOptions)
@@ -57,6 +61,31 @@ func WithRootfsCachePath(rootfsCachePath string) CreateSandboxOption {
5761
}
5862
}
5963

64+
// WithPreBootFn sets a callback that runs after the rootfs is ready but before
65+
// Firecracker boots. The callback receives the rootfs device path and can
66+
// modify filesystem on the host side.
67+
func WithPreBootFn(fn sandbox.PreBootFn) CreateSandboxOption {
68+
return func(opts *createSandboxOptions) {
69+
opts.preBootFn = fn
70+
}
71+
}
72+
73+
// ReservedBlocksOptions returns CreateSandboxOption(s) that set reserved blocks
74+
// on the rootfs before the guest boots, if the BuildReservedDiskSpaceMB feature
75+
// flag is greater than zero. Returns nil otherwise.
76+
func ReservedBlocksOptions(ctx context.Context, featureFlags *featureflags.Client, blockSize int64) []CreateSandboxOption {
77+
reservedDiskSpaceMB := int64(featureFlags.IntFlag(ctx, featureflags.BuildReservedDiskSpaceMB))
78+
if reservedDiskSpaceMB <= 0 {
79+
return nil
80+
}
81+
82+
return []CreateSandboxOption{
83+
WithPreBootFn(func(ctx context.Context, rootfsPath string) error {
84+
return filesystem.SetReservedBlocksOnHost(ctx, rootfsPath, reservedDiskSpaceMB, blockSize)
85+
}),
86+
}
87+
}
88+
6089
func NewCreateSandbox(config *sandbox.Config, sandboxFactory *sandbox.Factory, timeout time.Duration, options ...CreateSandboxOption) *CreateSandbox {
6190
opts := &createSandboxOptions{
6291
rootfsCachePath: "",
@@ -72,6 +101,7 @@ func NewCreateSandbox(config *sandbox.Config, sandboxFactory *sandbox.Factory, t
72101
rootfsCachePath: opts.rootfsCachePath,
73102
sandboxFactory: sandboxFactory,
74103
ioEngine: opts.ioEngine,
104+
preBootFn: opts.preBootFn,
75105
}
76106
}
77107

@@ -121,6 +151,7 @@ func (cs *CreateSandbox) Sandbox(
121151
IoEngine: cs.ioEngine,
122152
},
123153
nil,
154+
cs.preBootFn,
124155
)
125156
if err != nil {
126157
return nil, fmt.Errorf("create sandbox: %w", err)

packages/orchestrator/pkg/template/build/phases/base/builder.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,21 +252,19 @@ func (bb *BaseBuilder) buildLayerFromOCI(
252252
return metadata.Template{}, fmt.Errorf("error enlarging disk after provisioning: %w", err)
253253
}
254254

255-
if reservedDiskSpaceMB := int64(bb.featureFlags.IntFlag(ctx, featureflags.BuildReservedDiskSpaceMB)); reservedDiskSpaceMB > 0 {
256-
err = filesystem.SetReservedBlocksOnHost(ctx, rootfsPath, reservedDiskSpaceMB, bb.Config.RootfsBlockSize())
257-
if err != nil {
258-
return metadata.Template{}, fmt.Errorf("error setting reserved disk space: %w", err)
259-
}
260-
}
261-
262255
// Create sandbox for building template
263256
userLogger.Debug(ctx, "Creating base sandbox template layer")
264257

258+
sandboxOptions := []layer.CreateSandboxOption{
259+
layer.WithRootfsCachePath(rootfsPath),
260+
}
261+
sandboxOptions = append(sandboxOptions, layer.ReservedBlocksOptions(ctx, bb.featureFlags, bb.Config.RootfsBlockSize())...)
262+
265263
sandboxCreator := layer.NewCreateSandbox(
266264
baseSbxConfig,
267265
bb.sandboxFactory,
268266
baseLayerTimeout,
269-
layer.WithRootfsCachePath(rootfsPath),
267+
sandboxOptions...,
270268
)
271269

272270
actionExecutor := layer.NewFunctionAction(func(ctx context.Context, sbx *sandbox.Sandbox, meta metadata.Template) (metadata.Template, error) {

packages/orchestrator/pkg/template/build/phases/base/provision.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ func (bb *BaseBuilder) provisionSandbox(
139139
Stderr: logsWriter,
140140
},
141141
nil,
142+
nil, // no pre-boot hook for provisioning
142143
)
143144
if err != nil {
144145
return fmt.Errorf("error creating sandbox: %w", err)

packages/orchestrator/pkg/template/build/phases/finalize/builder.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,20 @@ func (ppb *PostProcessingBuilder) Build(
174174
span.SetAttributes(attribute.String("io_engine", ioEngine))
175175
ppb.logger.Debug(ctx, "using io engine", zap.String("io_engine", ioEngine))
176176

177+
// Collect sandbox creation options
178+
sandboxOptions := []layer.CreateSandboxOption{
179+
layer.WithIoEngine(ioEngine),
180+
}
181+
if sourceLayer.Cached {
182+
sandboxOptions = append(sandboxOptions, layer.ReservedBlocksOptions(ctx, ppb.featureFlags, ppb.Config.RootfsBlockSize())...)
183+
}
184+
177185
// Always restart the sandbox for the final layer to properly wire the rootfs path for the final template
178186
sandboxCreator := layer.NewCreateSandbox(
179187
sbxConfig,
180188
ppb.sandboxFactory,
181189
finalizeTimeout,
182-
layer.WithIoEngine(ioEngine),
190+
sandboxOptions...,
183191
)
184192

185193
actionExecutor := layer.NewFunctionAction(ppb.postProcessingFn(userLogger))
@@ -219,16 +227,6 @@ func (ppb *PostProcessingBuilder) postProcessingFn(userLogger logger.Logger) lay
219227
return
220228
}
221229

222-
// Set reserved disk space for the guest OS before syncing
223-
if reservedDiskSpaceMB := int64(ppb.featureFlags.IntFlag(ctx, featureflags.BuildReservedDiskSpaceMB)); reservedDiskSpaceMB > 0 {
224-
err := sandboxtools.SetReservedBlocksInGuest(ctx, ppb.proxy, userLogger, sbx.Runtime.SandboxID, reservedDiskSpaceMB, ppb.Config.RootfsBlockSize())
225-
if err != nil {
226-
e = fmt.Errorf("error setting reserved disk space: %w", err)
227-
228-
return
229-
}
230-
}
231-
232230
// Ensure all changes are synchronized to disk so the sandbox can be restarted
233231
err := sandboxtools.SyncChangesToDisk(
234232
ctx,

packages/orchestrator/pkg/template/build/phases/steps/builder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/sandboxtools"
2525
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/storage/cache"
2626
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/metadata"
27+
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
2728
templatemanager "github.com/e2b-dev/infra/packages/shared/pkg/grpc/template-manager"
2829
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
2930
)
@@ -47,6 +48,7 @@ type StepBuilder struct {
4748
commandExecutor *commands.CommandExecutor
4849
index cache.Index
4950
metrics *metrics.BuildMetrics
51+
featureFlags *featureflags.Client
5052
}
5153

5254
func New(
@@ -58,6 +60,7 @@ func New(
5860
commandExecutor *commands.CommandExecutor,
5961
index cache.Index,
6062
metrics *metrics.BuildMetrics,
63+
featureFlags *featureflags.Client,
6164
step *templatemanager.TemplateStep,
6265
stepNumber int,
6366
defaultLoggingLevel zapcore.Level,
@@ -77,6 +80,7 @@ func New(
7780
commandExecutor: commandExecutor,
7881
index: index,
7982
metrics: metrics,
83+
featureFlags: featureFlags,
8084
}
8185
}
8286

@@ -180,6 +184,7 @@ func (sb *StepBuilder) Build(
180184
sbxConfig,
181185
sb.sandboxFactory,
182186
layerTimeout,
187+
layer.ReservedBlocksOptions(ctx, sb.featureFlags, sb.Config.RootfsBlockSize())...,
183188
)
184189
} else {
185190
sandboxCreator = layer.NewResumeSandbox(sbxConfig, sb.sandboxFactory, layerTimeout)

packages/orchestrator/pkg/template/build/phases/steps/factory.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/metrics"
1212
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/phases"
1313
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/storage/cache"
14+
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
1415
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
1516
)
1617

@@ -25,6 +26,7 @@ func CreateStepPhases(
2526
commandExecutor *commands.CommandExecutor,
2627
index cache.Index,
2728
metrics *metrics.BuildMetrics,
29+
featureFlags *featureflags.Client,
2830
) []phases.BuilderPhase {
2931
steps := make([]phases.BuilderPhase, 0, len(bc.Config.Steps))
3032

@@ -39,6 +41,7 @@ func CreateStepPhases(
3941
commandExecutor,
4042
index,
4143
metrics,
44+
featureFlags,
4245
step,
4346
i+1, // stepNumber starts from 1
4447
defaultLoggingLevel,

packages/orchestrator/pkg/template/build/phases/user/builder.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/phases"
1616
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/phases/steps"
1717
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/storage/cache"
18+
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
1819
template_manager "github.com/e2b-dev/infra/packages/shared/pkg/grpc/template-manager"
1920
"github.com/e2b-dev/infra/packages/shared/pkg/logger"
2021
)
@@ -34,6 +35,7 @@ func New(
3435
commandExecutor *commands.CommandExecutor,
3536
index cache.Index,
3637
metrics *metrics.BuildMetrics,
38+
featureFlags *featureflags.Client,
3739
user string,
3840
force *bool,
3941
) *UserBuilder {
@@ -47,6 +49,7 @@ func New(
4749
commandExecutor,
4850
index,
4951
metrics,
52+
featureFlags,
5053
&template_manager.TemplateStep{
5154
Type: "USER",
5255
Args: []string{user, "true"},

packages/orchestrator/pkg/template/build/sandboxtools/command.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ import (
1313
"go.opentelemetry.io/otel/attribute"
1414
"go.opentelemetry.io/otel/codes"
1515
"go.opentelemetry.io/otel/trace"
16-
"go.uber.org/zap"
1716
"go.uber.org/zap/zapcore"
1817

1918
"github.com/e2b-dev/infra/packages/orchestrator/pkg/proxy"
2019
"github.com/e2b-dev/infra/packages/orchestrator/pkg/sandbox"
2120
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/build/core/rootfs"
2221
"github.com/e2b-dev/infra/packages/orchestrator/pkg/template/metadata"
23-
"github.com/e2b-dev/infra/packages/orchestrator/pkg/units"
2422
"github.com/e2b-dev/infra/packages/shared/pkg/grpc"
2523
"github.com/e2b-dev/infra/packages/shared/pkg/grpc/envd/process"
2624
"github.com/e2b-dev/infra/packages/shared/pkg/grpc/envd/process/processconnect"
@@ -234,38 +232,6 @@ func logStream(ctx context.Context, logger logger.Logger, lvl zapcore.Level, id
234232
}
235233
}
236234

237-
// SetReservedBlocksInGuest sets the number of reserved filesystem blocks inside the sandbox.
238-
// Reserved blocks are only usable by root (uid 0), protecting the guest OS from disk-full conditions.
239-
// Requires e2fsprogs (tune2fs) installed in the guest image (standard on Debian-based images).
240-
func SetReservedBlocksInGuest(
241-
ctx context.Context,
242-
proxy *proxy.SandboxProxy,
243-
logger logger.Logger,
244-
sandboxID string,
245-
reservedSpaceMB int64,
246-
blockSize int64,
247-
) error {
248-
if reservedSpaceMB <= 0 {
249-
return nil
250-
}
251-
252-
blocks := units.MBToBytes(reservedSpaceMB) / blockSize
253-
tuneCmd := fmt.Sprintf("tune2fs -r %d /dev/vda", blocks)
254-
255-
return RunCommandWithLogger(
256-
ctx,
257-
proxy,
258-
logger,
259-
zap.DebugLevel,
260-
"set-reserved-disk-space",
261-
sandboxID,
262-
tuneCmd,
263-
metadata.Context{
264-
User: "root",
265-
},
266-
)
267-
}
268-
269235
// syncChangesToDisk synchronizes filesystem changes to the filesystem
270236
// This is useful to ensure that all changes made in the sandbox are written to disk
271237
// to be able to re-create the sandbox without resume.

0 commit comments

Comments
 (0)