diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index adcaf05fa..04698ddc3 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -134,6 +134,24 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst resourceTags = toMapTags(request.Body.Tags) } + if request.Body.Ttl != nil && request.Body.ExpiresAt != nil { + return oapi.CreateInstance400JSONResponse{ + Code: "invalid_request", + Message: "ttl and expires_at are mutually exclusive", + }, nil + } + var ttl *time.Duration + if request.Body.Ttl != nil { + parsedTTL, err := parseTTL(*request.Body.Ttl) + if err != nil { + return oapi.CreateInstance400JSONResponse{ + Code: "invalid_ttl", + Message: err.Error(), + }, nil + } + ttl = &parsedTTL + } + // Parse network enabled (default: true) networkEnabled := true if request.Body.Network != nil && request.Body.Network.Enabled != nil { @@ -314,6 +332,8 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst NetworkBandwidthUpload: networkBandwidthUpload, Env: env, Tags: resourceTags, + TTL: ttl, + ExpiresAt: request.Body.ExpiresAt, NetworkEnabled: networkEnabled, NetworkEgress: networkEgress, Credentials: credentials, @@ -363,6 +383,11 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst Code: "insufficient_resources", Message: err.Error(), }, nil + case errors.Is(err, instances.ErrInvalidExpiresAt): + return oapi.CreateInstance400JSONResponse{ + Code: "invalid_expires_at", + Message: err.Error(), + }, nil case errors.Is(err, instances.ErrInvalidRequest): return oapi.CreateInstance400JSONResponse{ Code: "invalid_request", @@ -986,9 +1011,8 @@ func (s *ApiService) StatInstancePath(ctx context.Context, request oapi.StatInst return response, nil } -// UpdateInstance updates mutable properties of a running instance. -// Currently supports updating env vars referenced by credential policies for key rotation. -// Note: Resolution is handled by ResolveResource middleware +// UpdateInstance updates mutable instance properties. +// Note: Resolution is handled by ResolveResource middleware. func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInstanceRequestObject) (oapi.UpdateInstanceResponseObject, error) { inst := mw.GetResolvedInstance[instances.Instance](ctx) if inst == nil { @@ -1031,6 +1055,23 @@ func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInst Message: err.Error(), }, nil } + if request.Body.Ttl != nil && request.Body.ExpiresAt != nil { + return oapi.UpdateInstance400JSONResponse{ + Code: "invalid_request", + Message: "ttl and expires_at are mutually exclusive", + }, nil + } + var ttl *time.Duration + if request.Body.Ttl != nil { + parsedTTL, err := parseTTL(*request.Body.Ttl) + if err != nil { + return oapi.UpdateInstance400JSONResponse{ + Code: "invalid_ttl", + Message: err.Error(), + }, nil + } + ttl = &parsedTTL + } result, err := s.InstanceManager.UpdateInstance(ctx, inst.Id, instances.UpdateInstanceRequest{ Env: env, @@ -1038,6 +1079,8 @@ func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInst HealthCheck: healthCheck, RestartPolicy: restartPolicy, RestartPolicySet: request.Body.RestartPolicy != nil, + TTL: ttl, + ExpiresAt: request.Body.ExpiresAt, }) if err != nil { switch { @@ -1051,6 +1094,16 @@ func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInst Code: "invalid_state", Message: err.Error(), }, nil + case errors.Is(err, instances.ErrInstanceExpired): + return oapi.UpdateInstance409JSONResponse{ + Code: "instance_expired", + Message: err.Error(), + }, nil + case errors.Is(err, instances.ErrInvalidExpiresAt): + return oapi.UpdateInstance400JSONResponse{ + Code: "invalid_expires_at", + Message: err.Error(), + }, nil case errors.Is(err, instances.ErrInvalidRequest): return oapi.UpdateInstance400JSONResponse{ Code: "invalid_request", @@ -1142,6 +1195,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance { DiskIoBps: diskIoBpsStr, Network: nil, CreatedAt: inst.CreatedAt, + ExpiresAt: inst.ExpiresAt, StartedAt: inst.StartedAt, StoppedAt: inst.StoppedAt, ExitCode: inst.ExitCode, @@ -1155,7 +1209,6 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance { if inst.Platform != "" { oapiInst.Platform = lo.ToPtr(inst.Platform) } - if inst.ExitMessage != "" { oapiInst.ExitMessage = lo.ToPtr(inst.ExitMessage) } @@ -1308,6 +1361,17 @@ func toOAPISnapshotPolicy(policy instances.SnapshotPolicy) oapi.SnapshotPolicy { return out } +func parseTTL(value string) (time.Duration, error) { + ttl, err := time.ParseDuration(value) + if err != nil { + return 0, fmt.Errorf("ttl must be a valid duration: %w", err) + } + if ttl < 0 { + return 0, fmt.Errorf("ttl cannot be negative") + } + return ttl, nil +} + func parseOptionalDuration(value string, field string) (*time.Duration, error) { duration, err := time.ParseDuration(value) if err != nil { diff --git a/cmd/api/api/instances_test.go b/cmd/api/api/instances_test.go index d23e7c21e..06abe3e54 100644 --- a/cmd/api/api/instances_test.go +++ b/cmd/api/api/instances_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "fmt" "os" "testing" @@ -284,6 +285,11 @@ func (m *captureUpdateManager) UpdateInstance(ctx context.Context, id string, re } now := time.Now() + expiresAt := req.ExpiresAt + if req.TTL != nil && *req.TTL > 0 { + resolved := now.Add(*req.TTL) + expiresAt = &resolved + } return &instances.Instance{ StoredMetadata: instances.StoredMetadata{ Id: id, @@ -294,6 +300,7 @@ func (m *captureUpdateManager) UpdateInstance(ctx context.Context, id string, re HealthCheck: req.HealthCheck, RestartPolicy: req.RestartPolicy, CreatedAt: now, + ExpiresAt: expiresAt, HypervisorType: hypervisor.TypeCloudHypervisor, }, State: instances.StateRunning, @@ -307,6 +314,11 @@ func (m *captureCreateManager) CreateInstance(ctx context.Context, req instances } now := time.Now() + expiresAt := req.ExpiresAt + if req.TTL != nil && *req.TTL > 0 { + resolved := now.Add(*req.TTL) + expiresAt = &resolved + } return &instances.Instance{ StoredMetadata: instances.StoredMetadata{ Id: "inst-hotplug-default", @@ -320,12 +332,144 @@ func (m *captureCreateManager) CreateInstance(ctx context.Context, req instances HealthCheck: req.HealthCheck, RestartPolicy: req.RestartPolicy, CreatedAt: now, + ExpiresAt: expiresAt, HypervisorType: hypervisor.TypeCloudHypervisor, }, State: instances.StateRunning, }, nil } +func TestCreateInstance_TTL(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureCreateManager(svc.InstanceManager) + svc.InstanceManager = mockMgr + ttl := "90m" + + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{Body: &oapi.CreateInstanceRequest{ + Name: "test-ttl", + Image: "docker.io/library/alpine:latest", + Ttl: &ttl, + }}) + require.NoError(t, err) + created, ok := resp.(oapi.CreateInstance201JSONResponse) + require.True(t, ok) + require.NotNil(t, mockMgr.lastReq) + require.NotNil(t, mockMgr.lastReq.TTL) + assert.Equal(t, 90*time.Minute, *mockMgr.lastReq.TTL) + require.NotNil(t, created.ExpiresAt) + assert.Equal(t, created.CreatedAt.Add(90*time.Minute), *created.ExpiresAt) +} + +func TestCreateInstance_ZeroTTLDisablesExpiration(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureCreateManager(svc.InstanceManager) + svc.InstanceManager = mockMgr + ttl := "0s" + + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{Body: &oapi.CreateInstanceRequest{ + Name: "test-no-expiration", + Image: "docker.io/library/alpine:latest", + Ttl: &ttl, + }}) + require.NoError(t, err) + created, ok := resp.(oapi.CreateInstance201JSONResponse) + require.True(t, ok) + require.NotNil(t, mockMgr.lastReq.TTL) + assert.Zero(t, *mockMgr.lastReq.TTL) + assert.Nil(t, created.ExpiresAt) +} + +func TestCreateInstance_ExpiresAt(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureCreateManager(svc.InstanceManager) + svc.InstanceManager = mockMgr + expiresAt := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) + + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{Body: &oapi.CreateInstanceRequest{ + Name: "test-expires-at", + Image: "docker.io/library/alpine:latest", + ExpiresAt: &expiresAt, + }}) + require.NoError(t, err) + created, ok := resp.(oapi.CreateInstance201JSONResponse) + require.True(t, ok) + require.NotNil(t, mockMgr.lastReq.ExpiresAt) + assert.Equal(t, expiresAt, *mockMgr.lastReq.ExpiresAt) + require.NotNil(t, created.ExpiresAt) + assert.Equal(t, expiresAt, *created.ExpiresAt) +} + +func TestCreateInstance_MapsInvalidExpiresAt(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureCreateManager(svc.InstanceManager) + mockMgr.err = fmt.Errorf("%w: must be in the future", instances.ErrInvalidExpiresAt) + svc.InstanceManager = mockMgr + expiresAt := time.Now().Add(-time.Hour) + + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{Body: &oapi.CreateInstanceRequest{ + Name: "test-expired-deadline", + Image: "docker.io/library/alpine:latest", + ExpiresAt: &expiresAt, + }}) + require.NoError(t, err) + badRequest, ok := resp.(oapi.CreateInstance400JSONResponse) + require.True(t, ok) + assert.Equal(t, "invalid_expires_at", badRequest.Code) +} + +func TestInstanceResponseIncludesNullExpiresAt(t *testing.T) { + t.Parallel() + + encoded, err := json.Marshal(instanceToOAPI(instances.Instance{})) + require.NoError(t, err) + assert.Contains(t, string(encoded), `"expires_at":null`) +} + +func TestCreateInstance_RejectsConflictingExpiration(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + ttl := "1h" + expiresAt := time.Now().Add(2 * time.Hour) + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{Body: &oapi.CreateInstanceRequest{ + Name: "test-conflicting-expiration", + Image: "docker.io/library/alpine:latest", + Ttl: &ttl, + ExpiresAt: &expiresAt, + }}) + require.NoError(t, err) + badRequest, ok := resp.(oapi.CreateInstance400JSONResponse) + require.True(t, ok) + assert.Equal(t, "invalid_request", badRequest.Code) +} + +func TestCreateInstance_InvalidTTL(t *testing.T) { + t.Parallel() + + for _, ttl := range []string{"not-a-duration", "-1h"} { + t.Run(ttl, func(t *testing.T) { + svc := newTestService(t) + resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{Body: &oapi.CreateInstanceRequest{ + Name: "test-invalid-ttl", + Image: "docker.io/library/alpine:latest", + Ttl: &ttl, + }}) + require.NoError(t, err) + badRequest, ok := resp.(oapi.CreateInstance400JSONResponse) + require.True(t, ok) + assert.Equal(t, "invalid_ttl", badRequest.Code) + }) + } +} + func TestCreateInstance_MapsQEMUMicroVMHypervisor(t *testing.T) { t.Parallel() @@ -1067,6 +1211,92 @@ func TestUpdateInstance_MapsEnvPatch(t *testing.T) { assert.Equal(t, "rotated-key-456", mockMgr.lastReq.Env["OUTBOUND_OPENAI_KEY"]) } +func TestUpdateInstance_MapsTTL(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureUpdateManager(svc.InstanceManager) + svc.InstanceManager = mockMgr + resolved := &instances.Instance{StoredMetadata: instances.StoredMetadata{Id: "inst-update-ttl"}} + ttl := "6h" + + resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{ + Id: resolved.Id, + Body: &oapi.UpdateInstanceRequest{Ttl: &ttl}, + }) + require.NoError(t, err) + updated, ok := resp.(oapi.UpdateInstance200JSONResponse) + require.True(t, ok) + require.NotNil(t, mockMgr.lastReq.TTL) + assert.Equal(t, 6*time.Hour, *mockMgr.lastReq.TTL) + require.NotNil(t, updated.ExpiresAt) + assert.WithinDuration(t, time.Now().Add(6*time.Hour), *updated.ExpiresAt, time.Second) +} + +func TestUpdateInstance_ZeroTTLDisablesExpiration(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureUpdateManager(svc.InstanceManager) + svc.InstanceManager = mockMgr + resolved := &instances.Instance{StoredMetadata: instances.StoredMetadata{Id: "inst-disable-expiration"}} + ttl := "0s" + + resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{ + Id: resolved.Id, + Body: &oapi.UpdateInstanceRequest{Ttl: &ttl}, + }) + require.NoError(t, err) + updated, ok := resp.(oapi.UpdateInstance200JSONResponse) + require.True(t, ok) + require.NotNil(t, mockMgr.lastReq.TTL) + assert.Zero(t, *mockMgr.lastReq.TTL) + assert.Nil(t, updated.ExpiresAt) +} + +func TestUpdateInstance_MapsExpiresAt(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureUpdateManager(svc.InstanceManager) + svc.InstanceManager = mockMgr + resolved := &instances.Instance{StoredMetadata: instances.StoredMetadata{Id: "inst-update-expires-at"}} + expiresAt := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) + + resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{ + Id: resolved.Id, + Body: &oapi.UpdateInstanceRequest{ExpiresAt: &expiresAt}, + }) + require.NoError(t, err) + updated, ok := resp.(oapi.UpdateInstance200JSONResponse) + require.True(t, ok) + require.NotNil(t, mockMgr.lastReq.ExpiresAt) + assert.Equal(t, expiresAt, *mockMgr.lastReq.ExpiresAt) + require.NotNil(t, updated.ExpiresAt) + assert.Equal(t, expiresAt, *updated.ExpiresAt) +} + +func TestUpdateInstance_RejectsConflictingExpiration(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + resolved := &instances.Instance{StoredMetadata: instances.StoredMetadata{Id: "inst-conflicting-expiration"}} + ttl := "1h" + expiresAt := time.Now().Add(2 * time.Hour) + + resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{ + Id: resolved.Id, + Body: &oapi.UpdateInstanceRequest{ + Ttl: &ttl, + ExpiresAt: &expiresAt, + }, + }) + require.NoError(t, err) + badRequest, ok := resp.(oapi.UpdateInstance400JSONResponse) + require.True(t, ok) + assert.Equal(t, "invalid_request", badRequest.Code) +} + func TestUpdateInstance_MapsAutoStandbyPatch(t *testing.T) { t.Parallel() svc := newTestService(t) @@ -1365,6 +1595,46 @@ func TestUpdateInstance_RequiresBody(t *testing.T) { assert.Contains(t, badReq.Message, "request body is required") } +func TestUpdateInstance_MapsInvalidExpiresAt(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureUpdateManager(svc.InstanceManager) + mockMgr.err = fmt.Errorf("%w: must be in the future", instances.ErrInvalidExpiresAt) + svc.InstanceManager = mockMgr + resolved := &instances.Instance{StoredMetadata: instances.StoredMetadata{Id: "inst-invalid-expires-at"}} + expiresAt := time.Now().Add(-time.Hour) + + resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{ + Id: resolved.Id, + Body: &oapi.UpdateInstanceRequest{ExpiresAt: &expiresAt}, + }) + require.NoError(t, err) + badRequest, ok := resp.(oapi.UpdateInstance400JSONResponse) + require.True(t, ok) + assert.Equal(t, "invalid_expires_at", badRequest.Code) +} + +func TestUpdateInstance_MapsExpiredError(t *testing.T) { + t.Parallel() + + svc := newTestService(t) + mockMgr := newCaptureUpdateManager(svc.InstanceManager) + mockMgr.err = fmt.Errorf("%w: expiration deadline has passed", instances.ErrInstanceExpired) + svc.InstanceManager = mockMgr + resolved := &instances.Instance{StoredMetadata: instances.StoredMetadata{Id: "inst-expired"}} + ttl := "1h" + + resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{ + Id: resolved.Id, + Body: &oapi.UpdateInstanceRequest{Ttl: &ttl}, + }) + require.NoError(t, err) + conflict, ok := resp.(oapi.UpdateInstance409JSONResponse) + require.True(t, ok) + assert.Equal(t, "instance_expired", conflict.Code) +} + func TestUpdateInstance_MapsInvalidRequestError(t *testing.T) { t.Parallel() svc := newTestService(t) diff --git a/cmd/api/main.go b/cmd/api/main.go index 04eb38d29..f1bbfcf3d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -663,6 +663,14 @@ func run() error { return restartController.StartRestartPolicyController(gctx) }) } + if ttlReaper, ok := app.InstanceManager.(interface { + StartTTLReaper(context.Context) error + }); ok { + grp.Go(func() error { + logger.Info("starting instance ttl reaper") + return ttlReaper.StartTTLReaper(gctx) + }) + } logger.Info("hypeman initialization complete", "duration", time.Since(startupStarted)) diff --git a/lib/instances/create.go b/lib/instances/create.go index 4e2245b73..0d21e8e91 100644 --- a/lib/instances/create.go +++ b/lib/instances/create.go @@ -96,6 +96,9 @@ func (m *manager) createInstance( log.ErrorContext(ctx, "invalid create request", "error", err) return nil, err } + if err := validateExpiresAt(req.ExpiresAt, m.nowUTC()); err != nil { + return nil, err + } if req.GPU != nil && req.GPU.Profile != "" && !devices.Capabilities().SupportsVGPU { return nil, fmt.Errorf("%w: %w", ErrInvalidRequest, devices.ErrVGPUNotSupportedOnMacOS) } @@ -172,6 +175,17 @@ func (m *manager) createInstance( ctx = enrichInstancesTrace(ctx, attribute.String("instance_id", id)) log.DebugContext(ctx, "generated instance ID", "instance_id", id) + // Keep the reaper from deleting metadata while creation is still in flight. + instanceLock, ownsInstanceLock := m.loadOrStoreInstanceLock(id) + removeInstanceLockOnFailure := ownsInstanceLock + instanceLock.Lock() + defer func() { + if retErr != nil && removeInstanceLockOnFailure { + m.instanceLocks.CompareAndDelete(id, instanceLock) + } + instanceLock.Unlock() + }() + // 4. Generate vsock configuration vsockCID := generateVsockCID(id) vsockSocket := m.paths.InstanceSocket(id, hypervisor.VsockSocketNameForType(hvType)) @@ -179,6 +193,7 @@ func (m *manager) createInstance( // 5. Check instance doesn't already exist if _, err := m.loadMetadata(id); err == nil { + removeInstanceLockOnFailure = false return nil, ErrAlreadyExists } @@ -329,6 +344,11 @@ func (m *manager) createInstance( } // 11. Create instance metadata + createdAt := m.nowUTC() + expiresAt, err := resolveCreateExpiration(req, createdAt) + if err != nil { + return nil, err + } stored := &StoredMetadata{ Id: id, Name: req.Name, @@ -347,7 +367,8 @@ func (m *manager) createInstance( NetworkEnabled: req.NetworkEnabled, NetworkEgress: cloneNetworkEgressPolicy(req.NetworkEgress), Credentials: cloneCredentialPolicies(req.Credentials), - CreatedAt: time.Now(), + CreatedAt: createdAt, + ExpiresAt: expiresAt, StartedAt: nil, StoppedAt: nil, ProgramStartedAt: nil, @@ -561,6 +582,21 @@ func (m *manager) createInstance( return &finalInst, nil } +func resolveCreateExpiration(req CreateInstanceRequest, now time.Time) (*time.Time, error) { + if req.ExpiresAt != nil { + if err := validateExpiresAt(req.ExpiresAt, now); err != nil { + return nil, err + } + expiresAt := req.ExpiresAt.UTC() + return &expiresAt, nil + } + if req.TTL == nil || *req.TTL == 0 { + return nil, nil + } + expiresAt := now.Add(*req.TTL) + return &expiresAt, nil +} + // validateCreateRequest validates the create instance request. // The request is mutated in-place to persist normalized egress/credential policy fields. func validateCreateRequest(req *CreateInstanceRequest) error { @@ -585,6 +621,12 @@ func validateCreateRequest(req *CreateInstanceRequest) error { if req.Vcpus < 0 { return fmt.Errorf("vcpus cannot be negative") } + if req.TTL != nil && *req.TTL < 0 { + return fmt.Errorf("%w: ttl cannot be negative", ErrInvalidRequest) + } + if req.TTL != nil && req.ExpiresAt != nil { + return fmt.Errorf("%w: ttl and expires_at are mutually exclusive", ErrInvalidRequest) + } if req.NetworkEgress != nil && req.NetworkEgress.Enabled { if !req.NetworkEnabled { return fmt.Errorf("%w: network.egress requires network.enabled=true", ErrInvalidRequest) diff --git a/lib/instances/errors.go b/lib/instances/errors.go index c61f10392..8d7042eb5 100644 --- a/lib/instances/errors.go +++ b/lib/instances/errors.go @@ -12,6 +12,12 @@ var ( // ErrInvalidRequest is returned when request validation fails ErrInvalidRequest = errors.New("invalid request") + // ErrInstanceExpired is returned when an expiration update loses the expiry race. + ErrInstanceExpired = errors.New("instance expired") + + // ErrInvalidExpiresAt is returned when an absolute expiration is not in the future. + ErrInvalidExpiresAt = errors.New("invalid expires_at") + // ErrAlreadyExists is returned when creating an instance that already exists ErrAlreadyExists = errors.New("instance already exists") diff --git a/lib/instances/expiration.go b/lib/instances/expiration.go new file mode 100644 index 000000000..d14e63511 --- /dev/null +++ b/lib/instances/expiration.go @@ -0,0 +1,13 @@ +package instances + +import ( + "fmt" + "time" +) + +func validateExpiresAt(expiresAt *time.Time, now time.Time) error { + if expiresAt != nil && !expiresAt.After(now) { + return fmt.Errorf("%w: must be in the future", ErrInvalidExpiresAt) + } + return nil +} diff --git a/lib/instances/fork.go b/lib/instances/fork.go index ea6d3a4c5..d65d5131b 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -278,6 +278,7 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin forkMeta.Id = forkID forkMeta.Name = req.Name forkMeta.CreatedAt = now + forkMeta.ExpiresAt = nil forkMeta.StartedAt = nil forkMeta.StoppedAt = nil forkMeta.HypervisorPID = nil @@ -613,6 +614,10 @@ func cloneStoredMetadata(src StoredMetadata) StoredMetadata { pid := *src.HypervisorPID dst.HypervisorPID = &pid } + if src.ExpiresAt != nil { + expiresAt := *src.ExpiresAt + dst.ExpiresAt = &expiresAt + } if src.StartedAt != nil { startedAt := *src.StartedAt dst.StartedAt = &startedAt @@ -643,3 +648,9 @@ func cloneStoredMetadataWithoutPendingStandbyCompression(src StoredMetadata) Sto dst.PendingStandbyCompression = nil return dst } + +func cloneStoredMetadataForSnapshot(src StoredMetadata) StoredMetadata { + dst := cloneStoredMetadataWithoutPendingStandbyCompression(src) + dst.ExpiresAt = nil + return dst +} diff --git a/lib/instances/fork_test.go b/lib/instances/fork_test.go index d51a687ef..32dc06ee6 100644 --- a/lib/instances/fork_test.go +++ b/lib/instances/fork_test.go @@ -577,6 +577,11 @@ func TestForkInstanceFromStandbyCancelsCompressionJobAndCopiesRawMemory(t *testi sourceID := "fork-standby-compressed-src" createStandbySnapshotSourceFixture(t, manager, sourceID, sourceID, manager.defaultHypervisor) + expiresAt := time.Now().Add(time.Hour) + sourceMeta, err := manager.loadMetadata(sourceID) + require.NoError(t, err) + sourceMeta.ExpiresAt = &expiresAt + require.NoError(t, manager.saveMetadata(sourceMeta)) rawPath := filepath.Join(manager.paths.InstanceSnapshotLatest(sourceID), "memory-ranges") require.NoError(t, os.WriteFile(rawPath, []byte("some guest memory"), 0o644)) @@ -584,7 +589,7 @@ func TestForkInstanceFromStandbyCancelsCompressionJobAndCopiesRawMemory(t *testi require.NoError(t, os.MkdirAll(filepath.Dir(snapshotConfigPath), 0o755)) require.NoError(t, os.WriteFile(snapshotConfigPath, []byte(`{}`), 0o644)) - _, _, err := compressSnapshotMemoryFile(ctx, rawPath, snapshotstore.SnapshotCompressionConfig{ + _, _, err = compressSnapshotMemoryFile(ctx, rawPath, snapshotstore.SnapshotCompressionConfig{ Enabled: true, Algorithm: snapshotstore.SnapshotCompressionAlgorithmZstd, Level: intPtr(1), @@ -619,6 +624,7 @@ func TestForkInstanceFromStandbyCancelsCompressionJobAndCopiesRawMemory(t *testi }, true) require.NoError(t, err) require.NotNil(t, forked) + assert.Nil(t, forked.ExpiresAt) assert.True(t, canceled.Load(), "standby compression job should be canceled before copying the source guest directory") @@ -671,6 +677,7 @@ func TestCloneStoredMetadataForFork_DeepCopiesReferenceFields(t *testing.T) { t.Parallel() startedAt := time.Now().Add(-2 * time.Minute) stoppedAt := time.Now().Add(-1 * time.Minute) + expiresAt := time.Now().Add(time.Hour) notBefore := time.Now().Add(5 * time.Minute) pid := 1234 exitCode := 17 @@ -687,6 +694,7 @@ func TestCloneStoredMetadataForFork_DeepCopiesReferenceFields(t *testing.T) { Devices: []string{"0000:01:00.0"}, Entrypoint: []string{"/bin/sh", "-c"}, Cmd: []string{"echo", "hello"}, + ExpiresAt: &expiresAt, StartedAt: &startedAt, StoppedAt: &stoppedAt, HypervisorPID: &pid, @@ -741,6 +749,7 @@ func TestCloneStoredMetadataForFork_DeepCopiesReferenceFields(t *testing.T) { now := time.Now() *cloned.PendingStandbyCompression.Policy.Level = 1 cloned.PendingStandbyCompression.NotBefore = now + *cloned.ExpiresAt = now *cloned.StartedAt = now *cloned.StoppedAt = now @@ -760,6 +769,7 @@ func TestCloneStoredMetadataForFork_DeepCopiesReferenceFields(t *testing.T) { require.NotNil(t, src.PendingStandbyCompression.Policy.Level) require.Equal(t, 3, *src.PendingStandbyCompression.Policy.Level) require.Equal(t, notBefore, src.PendingStandbyCompression.NotBefore) + require.Equal(t, expiresAt, *src.ExpiresAt) require.Equal(t, startedAt, *src.StartedAt) require.Equal(t, stoppedAt, *src.StoppedAt) } diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 5d595d823..8855ba4f8 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -179,7 +179,9 @@ type manager struct { tracer trace.Tracer now func() time.Time writeFile func(string, []byte, os.FileMode) error + deleteInstanceFn func(context.Context, string) error deleteSnapshotFn func(context.Context, string) error + ttlReaperDeleteTimeout time.Duration egressProxy *egressproxy.Service egressProxyServiceOptions egressproxy.ServiceOptions egressProxyMu sync.Mutex @@ -376,8 +378,13 @@ func (m *manager) supportsConcurrentForkPrepare(hvType hypervisor.Type) bool { // getInstanceLock returns or creates a lock for a specific instance func (m *manager) getInstanceLock(id string) *sync.RWMutex { - lock, _ := m.instanceLocks.LoadOrStore(id, &sync.RWMutex{}) - return lock.(*sync.RWMutex) + lock, _ := m.loadOrStoreInstanceLock(id) + return lock +} + +func (m *manager) loadOrStoreInstanceLock(id string) (*sync.RWMutex, bool) { + lock, loaded := m.instanceLocks.LoadOrStore(id, &sync.RWMutex{}) + return lock.(*sync.RWMutex), !loaded } // maybePersistExitInfo persists exit info to metadata under the instance write lock. @@ -440,10 +447,17 @@ func (m *manager) DeleteInstance(ctx context.Context, id string) error { lock.Lock() defer lock.Unlock() - err := m.deleteInstance(ctx, id) + return m.deleteInstanceLocked(ctx, id) +} + +func (m *manager) deleteInstanceLocked(ctx context.Context, id string) error { + deleteInstance := m.deleteInstance + if m.deleteInstanceFn != nil { + deleteInstance = m.deleteInstanceFn + } + err := deleteInstance(ctx, id) if err == nil { m.notifyLifecycleDelete(ctx, id) - // Clean up the lock after successful deletion m.instanceLocks.Delete(id) } return err diff --git a/lib/instances/metrics.go b/lib/instances/metrics.go index 494cc2402..1ada5ac1e 100644 --- a/lib/instances/metrics.go +++ b/lib/instances/metrics.go @@ -93,6 +93,7 @@ type Metrics struct { snapshotCompressionPreemptionsTotal metric.Int64Counter lifecycleEventsDroppedTotal metric.Int64Counter forkMemFileShareFallbacksTotal metric.Int64Counter + ttlReaperDeletionsTotal metric.Int64Counter tracer trace.Tracer } @@ -261,6 +262,14 @@ func newInstanceMetrics(meter metric.Meter, tracer trace.Tracer, m *manager) (*M return nil, err } + ttlReaperDeletionsTotal, err := meter.Int64Counter( + "hypeman_instances_ttl_reaper_deletions_total", + metric.WithDescription("Total number of instance TTL reaper deletion attempts"), + ) + if err != nil { + return nil, err + } + // Register observable gauge for instance counts by state instancesTotal, err := meter.Int64ObservableGauge( "hypeman_instances_total", @@ -454,6 +463,7 @@ func newInstanceMetrics(meter metric.Meter, tracer trace.Tracer, m *manager) (*M snapshotCompressionPreemptionsTotal: snapshotCompressionPreemptionsTotal, lifecycleEventsDroppedTotal: lifecycleEventsDroppedTotal, forkMemFileShareFallbacksTotal: forkMemFileShareFallbacksTotal, + ttlReaperDeletionsTotal: ttlReaperDeletionsTotal, tracer: tracer, }, nil } diff --git a/lib/instances/snapshot.go b/lib/instances/snapshot.go index 669d087f0..eef7a6092 100644 --- a/lib/instances/snapshot.go +++ b/lib/instances/snapshot.go @@ -151,7 +151,7 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps SourceHypervisor: stored.HypervisorType, CreatedAt: time.Now(), }, - StoredMetadata: cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata), + StoredMetadata: cloneStoredMetadataForSnapshot(meta.StoredMetadata), } sizeBytes, err := snapshotstore.DirectoryFileSize(snapshotGuestDir) if err != nil { @@ -203,7 +203,7 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps SourceHypervisor: stored.HypervisorType, CreatedAt: time.Now(), }, - StoredMetadata: cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata), + StoredMetadata: cloneStoredMetadataForSnapshot(meta.StoredMetadata), } sizeBytes, err := snapshotstore.DirectoryFileSize(snapshotGuestDir) if err != nil { @@ -299,6 +299,7 @@ func (m *manager) restoreSnapshot(ctx context.Context, id string, snapshotID str restored := cloneStoredMetadataWithoutPendingStandbyCompression(rec.StoredMetadata) restored.Id = sourceMeta.Id restored.Name = sourceMeta.Name + restored.ExpiresAt = sourceMeta.ExpiresAt restored.DataDir = m.paths.InstanceDir(id) restored.HypervisorPID = nil restored.StartedAt = nil @@ -422,6 +423,7 @@ func (m *manager) forkSnapshot(ctx context.Context, snapshotID string, req ForkS forkMeta.Id = forkID forkMeta.Name = req.Name forkMeta.CreatedAt = now + forkMeta.ExpiresAt = nil forkMeta.StartedAt = nil forkMeta.StoppedAt = nil forkMeta.HypervisorPID = nil diff --git a/lib/instances/snapshot_test.go b/lib/instances/snapshot_test.go index 144fdc478..917f88d46 100644 --- a/lib/instances/snapshot_test.go +++ b/lib/instances/snapshot_test.go @@ -23,6 +23,11 @@ func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { hvType := mgr.defaultHypervisor sourceID := "snapshot-stopped-src" createStoppedSnapshotSourceFixture(t, mgr, sourceID, "snapshot-stopped-src", hvType) + expiresAt := time.Now().Add(24 * time.Hour) + sourceMeta, err := mgr.loadMetadata(sourceID) + require.NoError(t, err) + sourceMeta.ExpiresAt = &expiresAt + require.NoError(t, mgr.saveMetadata(sourceMeta)) snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ Kind: SnapshotKindStopped, @@ -30,6 +35,9 @@ func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { }) require.NoError(t, err) require.Equal(t, SnapshotKindStopped, snap.Kind) + snapshotMeta, err := mgr.loadSnapshotRecord(snap.Id) + require.NoError(t, err) + assert.Nil(t, snapshotMeta.StoredMetadata.ExpiresAt) restored, err := mgr.RestoreSnapshot(ctx, sourceID, snap.Id, RestoreSnapshotRequest{ TargetState: StateStopped, @@ -38,6 +46,8 @@ func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { require.NoError(t, err) require.Equal(t, StateStopped, restored.State) require.Equal(t, hvType, restored.HypervisorType) + require.NotNil(t, restored.ExpiresAt) + assert.True(t, expiresAt.Equal(*restored.ExpiresAt)) require.NoError(t, mgr.DeleteInstance(ctx, sourceID)) @@ -53,6 +63,7 @@ func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { require.NoError(t, err) require.Equal(t, StateStopped, forked.State) require.Equal(t, hvType, forked.HypervisorType) + assert.Nil(t, forked.ExpiresAt) t.Cleanup(func() { _ = deleteTestInstanceNow(context.Background(), mgr, forked.Id) }) require.NoError(t, mgr.DeleteSnapshot(ctx, snap.Id)) diff --git a/lib/instances/ttl_reaper.go b/lib/instances/ttl_reaper.go new file mode 100644 index 000000000..5c5fad974 --- /dev/null +++ b/lib/instances/ttl_reaper.go @@ -0,0 +1,139 @@ +package instances + +import ( + "context" + "errors" + "path/filepath" + "time" + + "github.com/kernel/hypeman/lib/logger" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +const ( + instanceTTLReaperInterval = time.Minute + instanceTTLReaperDeleteTimeout = 30 * time.Second +) + +// StartTTLReaper deletes instances after their configured expiration time. +func (m *manager) StartTTLReaper(ctx context.Context) error { + m.reapExpiredInstances(ctx) + + ticker := time.NewTicker(instanceTTLReaperInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + m.reapExpiredInstances(ctx) + } + } +} + +func (m *manager) reapExpiredInstances(ctx context.Context) { + log := logger.FromContext(ctx) + metaFiles, err := m.listMetadataFiles() + if err != nil { + log.ErrorContext(ctx, "instance ttl reaper failed to list metadata", "error", err) + return + } + + now := m.nowUTC() + for _, metaFile := range metaFiles { + if ctx.Err() != nil { + return + } + + id := filepath.Base(filepath.Dir(metaFile)) + meta, err := m.loadMetadata(id) + if errors.Is(err, ErrNotFound) { + continue + } + if err != nil { + log.ErrorContext(ctx, "instance ttl reaper failed to load metadata", "instance_id", id, "error", err) + continue + } + if !instanceExpired(&meta.StoredMetadata, now) { + continue + } + + deleted, err := m.reapExpiredInstanceWithTimeout(ctx, id) + + switch { + case err == nil && deleted: + m.recordTTLReaperDeletion(ctx, "success") + case err == nil: + continue + case errors.Is(err, context.DeadlineExceeded): + m.recordTTLReaperDeletion(ctx, "timeout") + log.ErrorContext(ctx, "instance ttl reaper timed out deleting instance", "instance_id", id, "error", err) + case errors.Is(err, ErrNotFound): + m.recordTTLReaperDeletion(ctx, "not_found") + default: + m.recordTTLReaperDeletion(ctx, "error") + log.ErrorContext(ctx, "instance ttl reaper failed to delete instance", "instance_id", id, "error", err) + } + } +} + +type ttlReaperDeleteResult struct { + deleted bool + err error +} + +func (m *manager) reapExpiredInstanceWithTimeout(ctx context.Context, id string) (bool, error) { + deleteCtx, cancel := context.WithTimeout(ctx, m.instanceTTLReaperDeleteTimeout()) + defer cancel() + + resultCh := make(chan ttlReaperDeleteResult, 1) + go func() { + deleted, err := m.reapExpiredInstance(deleteCtx, id) + resultCh <- ttlReaperDeleteResult{deleted: deleted, err: err} + }() + + select { + case result := <-resultCh: + return result.deleted, result.err + case <-deleteCtx.Done(): + return false, deleteCtx.Err() + } +} + +func (m *manager) reapExpiredInstance(ctx context.Context, id string) (bool, error) { + lock := m.getInstanceLock(id) + // Active lifecycle operations take precedence; retry on the next sweep. + if !lock.TryLock() { + return false, nil + } + defer lock.Unlock() + + meta, err := m.loadMetadata(id) + if err != nil { + return false, err + } + if !instanceExpired(&meta.StoredMetadata, m.nowUTC()) { + return false, nil + } + return true, m.deleteInstanceLocked(ctx, id) +} + +func (m *manager) instanceTTLReaperDeleteTimeout() time.Duration { + if m.ttlReaperDeleteTimeout > 0 { + return m.ttlReaperDeleteTimeout + } + return instanceTTLReaperDeleteTimeout +} + +func instanceExpired(meta *StoredMetadata, now time.Time) bool { + return meta != nil && meta.ExpiresAt != nil && !now.Before(*meta.ExpiresAt) +} + +func (m *manager) recordTTLReaperDeletion(ctx context.Context, status string) { + if m.metrics == nil { + return + } + m.metrics.ttlReaperDeletionsTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("status", status))) +} diff --git a/lib/instances/ttl_reaper_test.go b/lib/instances/ttl_reaper_test.go new file mode 100644 index 000000000..0e0cf9d71 --- /dev/null +++ b/lib/instances/ttl_reaper_test.go @@ -0,0 +1,263 @@ +package instances + +import ( + "context" + "os" + "testing" + "time" + + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveCreateExpiration(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + ttl := 6 * time.Hour + expiresAt, err := resolveCreateExpiration(CreateInstanceRequest{TTL: &ttl}, now) + require.NoError(t, err) + require.NotNil(t, expiresAt) + assert.Equal(t, now.Add(6*time.Hour), *expiresAt) + + expiresAt, err = resolveCreateExpiration(CreateInstanceRequest{}, now) + require.NoError(t, err) + assert.Nil(t, expiresAt) + + past := now.Add(-time.Second) + _, err = resolveCreateExpiration(CreateInstanceRequest{ExpiresAt: &past}, now) + require.ErrorIs(t, err, ErrInvalidExpiresAt) + + zero := time.Duration(0) + future := now.Add(time.Hour) + err = validateCreateRequest(&CreateInstanceRequest{ + Name: "conflicting-expiration", + Image: "alpine", + TTL: &zero, + ExpiresAt: &future, + }) + require.ErrorIs(t, err, ErrInvalidRequest) +} + +func TestInstanceExpired(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + before := now.Add(time.Hour) + at := now + after := now.Add(-time.Hour) + + for _, tc := range []struct { + name string + meta *StoredMetadata + want bool + }{ + {name: "before deadline", meta: &StoredMetadata{ExpiresAt: &before}}, + {name: "at deadline", meta: &StoredMetadata{ExpiresAt: &at}, want: true}, + {name: "after deadline", meta: &StoredMetadata{ExpiresAt: &after}, want: true}, + {name: "expiration disabled", meta: &StoredMetadata{}}, + {name: "missing metadata"}, + } { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, instanceExpired(tc.meta, now)) + }) + } +} + +func TestTTLReaperUsesPersistedExpirationAfterRestart(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + p := paths.New(t.TempDir()) + writer := &manager{paths: p} + + save := func(id string, expiresAt *time.Time) { + t.Helper() + require.NoError(t, writer.ensureDirectories(id)) + require.NoError(t, writer.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: expiresAt, + DataDir: p.InstanceDir(id), + }})) + } + + expiredAt := now.Add(-time.Minute) + future := now.Add(time.Hour) + save("expired", &expiredAt) + save("not-expired", &future) + save("no-expiration", nil) + + restarted := &manager{ + paths: p, + now: func() time.Time { return now }, + lifecycleEvents: newLifecycleSubscribersWithBufferSize(defaultLifecycleEventBufferSize), + } + restarted.reapExpiredInstances(context.Background()) + + _, err := os.Stat(p.InstanceDir("expired")) + require.ErrorIs(t, err, os.ErrNotExist) + for _, id := range []string{"not-expired", "no-expiration"} { + _, err := os.Stat(p.InstanceDir(id)) + require.NoError(t, err) + } +} + +func TestReapExpiredInstanceRechecksExpirationUnderLock(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + p := paths.New(t.TempDir()) + m := &manager{ + paths: p, + now: func() time.Time { return now }, + lifecycleEvents: newLifecycleSubscribersWithBufferSize(defaultLifecycleEventBufferSize), + } + id := "extended-before-reap" + require.NoError(t, m.ensureDirectories(id)) + expiredAt := now.Add(-time.Minute) + meta := &metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiredAt, + DataDir: p.InstanceDir(id), + }} + require.NoError(t, m.saveMetadata(meta)) + require.True(t, instanceExpired(&meta.StoredMetadata, now)) + + future := now.Add(time.Hour) + meta.ExpiresAt = &future + require.NoError(t, m.saveMetadata(meta)) + + deleted, err := m.reapExpiredInstance(context.Background(), id) + require.NoError(t, err) + assert.False(t, deleted) + _, err = os.Stat(p.InstanceDir(id)) + require.NoError(t, err) +} + +func TestStartTTLReaperSweepsBeforeFirstTick(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + p := paths.New(t.TempDir()) + m := &manager{ + paths: p, + now: func() time.Time { return now }, + lifecycleEvents: newLifecycleSubscribersWithBufferSize(defaultLifecycleEventBufferSize), + } + id := "expired-at-startup" + expiredAt := now.Add(-time.Minute) + require.NoError(t, m.ensureDirectories(id)) + require.NoError(t, m.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiredAt, + DataDir: p.InstanceDir(id), + }})) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + done := make(chan error, 1) + go func() { + done <- m.StartTTLReaper(ctx) + }() + + require.Eventually(t, func() bool { + _, err := os.Stat(p.InstanceDir(id)) + return os.IsNotExist(err) + }, time.Second, 10*time.Millisecond) + cancel() + select { + case err := <-done: + require.NoError(t, err) + case <-time.After(time.Second): + t.Fatal("ttl reaper did not stop after cancellation") + } +} + +func TestTTLReaperDeleteTimeoutDoesNotBlockSweep(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + p := paths.New(t.TempDir()) + m := &manager{ + paths: p, + now: func() time.Time { return now }, + lifecycleEvents: newLifecycleSubscribersWithBufferSize(defaultLifecycleEventBufferSize), + ttlReaperDeleteTimeout: 100 * time.Millisecond, + } + expiredAt := now.Add(-time.Minute) + for _, id := range []string{"a-timeout", "b-deleted"} { + require.NoError(t, m.ensureDirectories(id)) + require.NoError(t, m.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiredAt, + DataDir: p.InstanceDir(id), + }})) + } + + calls := make(chan string, 2) + releaseStuckDelete := make(chan struct{}) + stuckDeleteDone := make(chan struct{}) + m.deleteInstanceFn = func(_ context.Context, id string) error { + calls <- id + if id == "a-timeout" { + <-releaseStuckDelete + err := m.deleteInstanceData(id) + close(stuckDeleteDone) + return err + } + return m.deleteInstanceData(id) + } + + m.reapExpiredInstances(context.Background()) + + assert.Equal(t, "a-timeout", <-calls) + assert.Equal(t, "b-deleted", <-calls) + _, err := os.Stat(p.InstanceDir("a-timeout")) + require.NoError(t, err) + _, err = os.Stat(p.InstanceDir("b-deleted")) + require.ErrorIs(t, err, os.ErrNotExist) + + close(releaseStuckDelete) + select { + case <-stuckDeleteDone: + case <-time.After(time.Second): + t.Fatal("timed-out deletion did not finish after release") + } +} + +func TestReapExpiredInstanceSkipsBusyCreateLock(t *testing.T) { + t.Parallel() + + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + p := paths.New(t.TempDir()) + m := &manager{paths: p, now: func() time.Time { return now }} + id := "creating-instance" + expiredAt := now.Add(-time.Minute) + require.NoError(t, m.ensureDirectories(id)) + require.NoError(t, m.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiredAt, + DataDir: p.InstanceDir(id), + }})) + + lock := m.getInstanceLock(id) + lock.Lock() + deleted, err := m.reapExpiredInstance(context.Background(), id) + lock.Unlock() + + require.NoError(t, err) + assert.False(t, deleted) + _, err = os.Stat(p.InstanceDir(id)) + require.NoError(t, err) +} diff --git a/lib/instances/types.go b/lib/instances/types.go index df3ab7ff8..a90ecd574 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -115,6 +115,7 @@ type StoredMetadata struct { // Timestamps (stored for historical tracking) CreatedAt time.Time + ExpiresAt *time.Time // Absolute expiration time; nil disables expiration StartedAt *time.Time // Boot epoch start time (set on create/start; preserved across standby restore) StoppedAt *time.Time // Last time VM was stopped @@ -274,6 +275,8 @@ type CreateInstanceRequest struct { AutoStandby *autostandby.Policy // Optional automatic standby policy HealthCheck *healthcheck.Policy // Optional workload health check policy RestartPolicy *restartpolicy.Policy // Optional whole-instance restart policy + TTL *time.Duration // Optional lifetime from creation; zero disables expiration + ExpiresAt *time.Time // Optional absolute expiration time; mutually exclusive with TTL AllowSystemVolumeMounts bool // Internal only: permits attaching reserved system volumes. Never populated from API requests. SystemVolumeMountPaths []string // Internal only: exact system paths reserved volumes may mount at. } @@ -291,6 +294,8 @@ type UpdateInstanceRequest struct { HealthCheck *healthcheck.Policy // Replaces the persisted health check policy when non-nil RestartPolicy *restartpolicy.Policy // Replaces the persisted restart policy when non-nil RestartPolicySet bool // True when restart policy was present in the update request + TTL *time.Duration // Relative lifetime from when the update is committed; zero disables expiration + ExpiresAt *time.Time // Absolute expiration time; mutually exclusive with TTL } // ForkInstanceRequest is the domain request for forking an instance. diff --git a/lib/instances/update.go b/lib/instances/update.go index cdea9e4cf..f5a2a420a 100644 --- a/lib/instances/update.go +++ b/lib/instances/update.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "sort" + "time" "github.com/kernel/hypeman/lib/egressproxy" "github.com/kernel/hypeman/lib/logger" @@ -16,7 +17,7 @@ type updateInstanceRulesService interface { // updateInstance updates mutable instance properties. // Env updates recompute egress proxy header inject rules with the new secret -// values. Auto-standby updates only change persisted metadata. +// values. Other updates only change persisted metadata. func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInstanceRequest) (*Instance, error) { log := logger.FromContext(ctx) @@ -52,6 +53,9 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta if err := validateUpdateInstanceRequest(meta, req); err != nil { return nil, err } + if err := validateExpirationUpdate(meta, req, m.nowUTC()); err != nil { + return nil, err + } if len(req.Env) > 0 && inst.State != StateRunning && inst.State != StateInitializing { return nil, fmt.Errorf("%w: instance must be running or initializing to update env (current state: %s)", ErrInvalidState, inst.State) } @@ -68,6 +72,9 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta nextMeta.RestartStatus = restartStatusAfterPolicyUpdate(nextMeta.RestartStatus) } if len(req.Env) == 0 { + if err := applyExpirationUpdateAtCommit(nextMeta, req, m.nowUTC()); err != nil { + return nil, err + } if err := m.saveMetadata(nextMeta); err != nil { return nil, fmt.Errorf("save metadata: %w", err) } @@ -100,7 +107,13 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta return nil, fmt.Errorf("egress proxy service unavailable") } - if err := applyUpdatedInstanceEnv(ctx, log, id, nextMeta, prevEnv, nextEnv, m.saveMetadata, svc); err != nil { + saveAtCommit := func(meta *metadata) error { + if err := applyExpirationUpdateAtCommit(meta, req, m.nowUTC()); err != nil { + return err + } + return m.saveMetadata(meta) + } + if err := applyUpdatedInstanceEnv(ctx, log, id, nextMeta, prevEnv, nextEnv, saveAtCommit, svc); err != nil { return nil, err } @@ -113,9 +126,54 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta return updated, nil } +func applyExpirationUpdateAtCommit(meta *metadata, req UpdateInstanceRequest, now time.Time) error { + expiresAt, expirationSet, err := resolveExpirationUpdate(meta, req, now) + if err != nil { + return err + } + if expirationSet { + meta.ExpiresAt = expiresAt + } + return nil +} + +func validateExpirationUpdate(meta *metadata, req UpdateInstanceRequest, now time.Time) error { + if req.TTL == nil && req.ExpiresAt == nil { + return nil + } + if instanceExpired(&meta.StoredMetadata, now) { + return fmt.Errorf("%w: expiration deadline has passed", ErrInstanceExpired) + } + return validateExpiresAt(req.ExpiresAt, now) +} + +func resolveExpirationUpdate(meta *metadata, req UpdateInstanceRequest, now time.Time) (*time.Time, bool, error) { + if err := validateExpirationUpdate(meta, req, now); err != nil { + return nil, false, err + } + if req.TTL == nil && req.ExpiresAt == nil { + return nil, false, nil + } + if req.TTL != nil { + if *req.TTL == 0 { + return nil, true, nil + } + expiresAt := now.Add(*req.TTL) + return &expiresAt, true, nil + } + expiresAt := req.ExpiresAt.UTC() + return &expiresAt, true, nil +} + func validateUpdateInstanceRequest(meta *metadata, req UpdateInstanceRequest) error { - if len(req.Env) == 0 && req.AutoStandby == nil && req.HealthCheck == nil && !req.RestartPolicySet { - return fmt.Errorf("%w: request must include env, auto_standby, health_check, and/or restart_policy", ErrInvalidRequest) + if len(req.Env) == 0 && req.AutoStandby == nil && req.HealthCheck == nil && !req.RestartPolicySet && req.TTL == nil && req.ExpiresAt == nil { + return fmt.Errorf("%w: request must include env, auto_standby, health_check, restart_policy, ttl, and/or expires_at", ErrInvalidRequest) + } + if req.TTL != nil && req.ExpiresAt != nil { + return fmt.Errorf("%w: ttl and expires_at are mutually exclusive", ErrInvalidRequest) + } + if req.TTL != nil && *req.TTL < 0 { + return fmt.Errorf("%w: ttl cannot be negative", ErrInvalidRequest) } if req.HealthCheck != nil { if meta == nil { diff --git a/lib/instances/update_test.go b/lib/instances/update_test.go index f3f1e16f6..2c2ff54e2 100644 --- a/lib/instances/update_test.go +++ b/lib/instances/update_test.go @@ -30,7 +30,32 @@ func TestValidateUpdateInstanceRequest(t *testing.T) { err := validateUpdateInstanceRequest(baseMeta, UpdateInstanceRequest{}) require.Error(t, err) assert.ErrorIs(t, err, ErrInvalidRequest) - assert.Contains(t, err.Error(), "env, auto_standby, health_check, and/or restart_policy") + assert.Contains(t, err.Error(), "env, auto_standby, health_check, restart_policy, ttl, and/or expires_at") + }) + + t.Run("allows disabling expiration", func(t *testing.T) { + ttl := time.Duration(0) + err := validateUpdateInstanceRequest(baseMeta, UpdateInstanceRequest{TTL: &ttl}) + require.NoError(t, err) + }) + + t.Run("rejects conflicting expiration fields", func(t *testing.T) { + ttl := time.Hour + expiresAt := time.Now().Add(time.Hour) + err := validateUpdateInstanceRequest(baseMeta, UpdateInstanceRequest{TTL: &ttl, ExpiresAt: &expiresAt}) + require.ErrorIs(t, err, ErrInvalidRequest) + }) + + t.Run("rejects negative ttl", func(t *testing.T) { + ttl := -time.Second + err := validateUpdateInstanceRequest(baseMeta, UpdateInstanceRequest{TTL: &ttl}) + require.ErrorIs(t, err, ErrInvalidRequest) + }) + + t.Run("rejects past absolute expiration consistently", func(t *testing.T) { + now := time.Now() + _, _, err := resolveExpirationUpdate(baseMeta, UpdateInstanceRequest{ExpiresAt: &now}, now) + require.ErrorIs(t, err, ErrInvalidExpiresAt) }) t.Run("rejects instances without credential backed envs", func(t *testing.T) { @@ -401,3 +426,154 @@ func TestManagerUpdateInstanceHealthCheckOnlyPublishesLifecycleUpdate(t *testing t.Fatal("timed out waiting for lifecycle update event") } } + +func TestManagerUpdateInstanceTTLIsRelativeToUpdate(t *testing.T) { + t.Parallel() + + m, _ := setupTestManager(t) + requestTime := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + commitTime := requestTime.Add(2 * time.Minute) + nowCalls := 0 + m.now = func() time.Time { + nowCalls++ + if nowCalls == 1 { + return requestTime + } + return commitTime + } + id := "inst-update-ttl" + require.NoError(t, m.ensureDirectories(id)) + oldExpiration := requestTime.Add(time.Hour) + require.NoError(t, m.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: requestTime.Add(-24 * time.Hour), + ExpiresAt: &oldExpiration, + DataDir: m.paths.InstanceDir(id), + }})) + + ttl := 6 * time.Hour + updated, err := m.UpdateInstance(context.Background(), id, UpdateInstanceRequest{TTL: &ttl}) + require.NoError(t, err) + require.NotNil(t, updated.ExpiresAt) + assert.GreaterOrEqual(t, nowCalls, 2) + assert.Equal(t, commitTime.Add(ttl), *updated.ExpiresAt) + + restarted := &manager{paths: m.paths} + persisted, err := restarted.loadMetadata(id) + require.NoError(t, err) + require.NotNil(t, persisted.ExpiresAt) + assert.Equal(t, commitTime.Add(ttl), *persisted.ExpiresAt) +} + +func TestManagerUpdateInstanceRejectsExpirationThatPassesBeforeCommit(t *testing.T) { + t.Parallel() + + m, _ := setupTestManager(t) + requestTime := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + commitTime := requestTime.Add(2 * time.Minute) + nowCalls := 0 + m.now = func() time.Time { + nowCalls++ + if nowCalls == 1 { + return requestTime + } + return commitTime + } + id := "inst-expires-during-update" + expiresAt := requestTime.Add(time.Minute) + require.NoError(t, m.ensureDirectories(id)) + require.NoError(t, m.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: requestTime.Add(-time.Hour), + ExpiresAt: &expiresAt, + DataDir: m.paths.InstanceDir(id), + }})) + + ttl := time.Hour + _, err := m.UpdateInstance(context.Background(), id, UpdateInstanceRequest{TTL: &ttl}) + require.ErrorIs(t, err, ErrInstanceExpired) + assert.GreaterOrEqual(t, nowCalls, 2) + + persisted, err := m.loadMetadata(id) + require.NoError(t, err) + require.NotNil(t, persisted.ExpiresAt) + assert.Equal(t, expiresAt, *persisted.ExpiresAt) +} + +func TestManagerUpdateInstanceDisablePreventsStaleReaperDelete(t *testing.T) { + t.Parallel() + + manager, _ := setupTestManager(t) + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + manager.now = func() time.Time { return now } + id := "inst-disable-expiration" + require.NoError(t, manager.ensureDirectories(id)) + expiresAt := now.Add(time.Minute) + require.NoError(t, manager.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiresAt, + DataDir: manager.paths.InstanceDir(id), + }})) + + disable := time.Duration(0) + updated, err := manager.UpdateInstance(context.Background(), id, UpdateInstanceRequest{TTL: &disable}) + require.NoError(t, err) + assert.Nil(t, updated.ExpiresAt) + + now = now.Add(2 * time.Hour) + manager.reapExpiredInstances(context.Background()) + _, err = manager.loadMetadata(id) + require.NoError(t, err) +} + +func TestManagerUpdateInstanceExpirationLosesAfterDeadline(t *testing.T) { + t.Parallel() + + manager, _ := setupTestManager(t) + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + manager.now = func() time.Time { return now } + id := "inst-expired-before-update" + require.NoError(t, manager.ensureDirectories(id)) + expiresAt := now + require.NoError(t, manager.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiresAt, + DataDir: manager.paths.InstanceDir(id), + }})) + + ttl := time.Hour + _, err := manager.UpdateInstance(context.Background(), id, UpdateInstanceRequest{TTL: &ttl}) + require.ErrorIs(t, err, ErrInstanceExpired) +} + +func TestManagerUpdateInstanceExpirationLosesAfterReaperDelete(t *testing.T) { + t.Parallel() + + manager, _ := setupTestManager(t) + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + manager.now = func() time.Time { return now } + id := "inst-reaped-before-update" + require.NoError(t, manager.ensureDirectories(id)) + expiresAt := now + require.NoError(t, manager.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.Add(-time.Hour), + ExpiresAt: &expiresAt, + DataDir: manager.paths.InstanceDir(id), + }})) + + deleted, err := manager.reapExpiredInstance(context.Background(), id) + require.NoError(t, err) + require.True(t, deleted) + + ttl := time.Hour + _, err = manager.UpdateInstance(context.Background(), id, UpdateInstanceRequest{TTL: &ttl}) + require.ErrorIs(t, err, ErrNotFound) +} diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 52ab3b2c0..2ee09469e 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -704,6 +704,9 @@ type CreateInstanceRequest struct { // Env Environment variables Env *map[string]string `json:"env,omitempty"` + // ExpiresAt Absolute expiration time. Must be in the future and is mutually exclusive with ttl. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Gpu GPU configuration for the instance Gpu *GPUConfig `json:"gpu,omitempty"` @@ -766,6 +769,9 @@ type CreateInstanceRequest struct { // Tags User-defined key-value tags. Tags *Tags `json:"tags,omitempty"` + // Ttl Relative lifetime from instance creation, in Go duration format. Use "0s" or omit both expiration fields to disable automatic expiration. Mutually exclusive with expires_at. + Ttl *string `json:"ttl,omitempty"` + // Vcpus Number of virtual CPUs Vcpus *int `json:"vcpus,omitempty"` @@ -1240,6 +1246,9 @@ type Instance struct { // ExitMessage Human-readable description of exit (e.g., "command not found", "killed by signal 9 (SIGKILL) - OOM") ExitMessage *string `json:"exit_message,omitempty"` + // ExpiresAt Absolute expiration time, or null when automatic expiration is disabled. Instance TTL is cleared on fork. + ExpiresAt *time.Time `json:"expires_at"` + // Gpu GPU information attached to the instance Gpu *InstanceGPU `json:"gpu,omitempty"` @@ -1846,11 +1855,17 @@ type UpdateInstanceRequest struct { // are accepted. Use this to rotate real credential values without restarting the VM. Env *map[string]string `json:"env,omitempty"` + // ExpiresAt Absolute expiration time. Must be in the future and is mutually exclusive with ttl. + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // HealthCheck Workload health check policy. Health is reported separately from instance lifecycle state. HealthCheck *HealthCheck `json:"health_check,omitempty"` // RestartPolicy Whole-instance restart supervision policy. RestartPolicy *RestartPolicy `json:"restart_policy,omitempty"` + + // Ttl Relative lifetime from when this update is committed, in Go duration format. Use "0s" to disable automatic expiration. Mutually exclusive with expires_at. + Ttl *string `json:"ttl,omitempty"` } // Volume defines model for Volume. @@ -18818,381 +18833,385 @@ func (sh *strictHandler) GetVolume(w http.ResponseWriter, r *http.Request, id st // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "eSdugOoBAA==", + "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", + "VAd7z5/udj59+PT/BwAA//+uhiQVbu4BAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index b117d2035..eac70a5ee 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -530,6 +530,15 @@ components: $ref: "#/components/schemas/HealthCheck" restart_policy: $ref: "#/components/schemas/RestartPolicy" + ttl: + type: string + description: Relative lifetime from when this update is committed, in Go duration format. Use "0s" to disable automatic expiration. Mutually exclusive with expires_at. + example: "6h" + expires_at: + type: string + format: date-time + description: Absolute expiration time. Must be in the future and is mutually exclusive with ttl. + example: "2026-08-21T18:00:00Z" CreateInstanceRequest: type: object @@ -601,6 +610,15 @@ components: format: "Bearer ${value}" tags: $ref: "#/components/schemas/Tags" + ttl: + type: string + description: Relative lifetime from instance creation, in Go duration format. Use "0s" or omit both expiration fields to disable automatic expiration. Mutually exclusive with expires_at. + example: "24h" + expires_at: + type: string + format: date-time + description: Absolute expiration time. Must be in the future and is mutually exclusive with ttl. + example: "2026-08-21T18:00:00Z" network: type: object description: Network configuration for the instance @@ -977,7 +995,7 @@ components: Instance: type: object - required: [id, name, image, state, created_at] + required: [id, name, image, state, created_at, expires_at] properties: id: type: string @@ -1072,6 +1090,12 @@ components: format: date-time description: Creation timestamp (RFC3339) example: "2025-01-15T10:30:00Z" + expires_at: + type: string + format: date-time + nullable: true + description: Absolute expiration time, or null when automatic expiration is disabled. Instance TTL is cleared on fork. + example: "2026-08-21T18:00:00Z" started_at: type: string format: date-time @@ -2838,9 +2862,8 @@ paths: patch: summary: Update instance properties description: | - Update mutable properties of a running instance. Currently supports updating - only the environment variables referenced by existing credential policies, - enabling secret/key rotation without instance restart. + Update mutable instance properties. TTL values are relative to when the update + is committed. Expiration updates are rejected after the current deadline passes. operationId: updateInstance security: - bearerAuth: [] @@ -2877,7 +2900,7 @@ paths: schema: $ref: "#/components/schemas/Error" 409: - description: Invalid state (instance must be running or initializing) + description: Invalid state or instance expiration deadline already passed content: application/json: schema: