From 0336f8b20b4b3cf02f89be1159b08340a067167e Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:42:31 +0000 Subject: [PATCH] Add instance expiration reaper --- cmd/api/api/instances.go | 72 ++- cmd/api/api/instances_test.go | 268 +++++++++++ cmd/api/main.go | 8 + lib/instances/create.go | 44 +- lib/instances/errors.go | 6 + lib/instances/expiration.go | 13 + lib/instances/fork.go | 11 + lib/instances/fork_test.go | 12 +- lib/instances/manager.go | 22 +- lib/instances/metrics.go | 10 + lib/instances/snapshot.go | 6 +- lib/instances/snapshot_test.go | 11 + lib/instances/ttl_reaper.go | 119 +++++ lib/instances/ttl_reaper_test.go | 240 ++++++++++ lib/instances/types.go | 5 + lib/instances/update.go | 41 +- lib/instances/update_test.go | 133 +++++- lib/oapi/oapi.go | 769 ++++++++++++++++--------------- openapi.yaml | 33 +- 19 files changed, 1427 insertions(+), 396 deletions(-) create mode 100644 lib/instances/expiration.go create mode 100644 lib/instances/ttl_reaper.go create mode 100644 lib/instances/ttl_reaper_test.go diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index adcaf05fa..da534d388 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..5aab835b6 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 > 0 { + resolved := now.Add(req.TTL) + expiresAt = &resolved + } return &instances.Instance{ StoredMetadata: instances.StoredMetadata{ Id: "inst-hotplug-default", @@ -320,12 +332,142 @@ 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) + 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) + 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 +1209,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 +1593,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..59155950a 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 == 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 < 0 { + return fmt.Errorf("%w: ttl cannot be negative", ErrInvalidRequest) + } + if req.TTL > 0 && 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..16dccfdc0 --- /dev/null +++ b/lib/instances/ttl_reaper.go @@ -0,0 +1,119 @@ +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 + } + + deleteCtx, cancel := context.WithTimeout(ctx, m.instanceTTLReaperDeleteTimeout()) + deleted, err := m.reapExpiredInstance(deleteCtx, id) + timedOut := errors.Is(deleteCtx.Err(), context.DeadlineExceeded) + cancel() + + switch { + case err == nil && deleted: + m.recordTTLReaperDeletion(ctx, "success") + case err == nil: + continue + case timedOut: + 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) + } + } +} + +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..02548bf78 --- /dev/null +++ b/lib/instances/ttl_reaper_test.go @@ -0,0 +1,240 @@ +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) + expiresAt, err := resolveCreateExpiration(CreateInstanceRequest{TTL: 6 * time.Hour}, 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) +} + +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: 10 * 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), + }})) + } + + var calls []string + m.deleteInstanceFn = func(ctx context.Context, id string) error { + calls = append(calls, id) + if id == "a-timeout" { + <-ctx.Done() + return ctx.Err() + } + return m.deleteInstanceData(id) + } + + m.reapExpiredInstances(context.Background()) + + assert.Equal(t, []string{"a-timeout", "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) +} + +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..973fe93cf 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..221f73b49 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,10 +53,17 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta if err := validateUpdateInstanceRequest(meta, req); err != nil { return nil, err } + expiresAt, expirationSet, err := resolveExpirationUpdate(meta, req, m.nowUTC()) + if 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) } nextMeta := deepCopyMetadata(meta) + if expirationSet { + nextMeta.ExpiresAt = expiresAt + } if req.AutoStandby != nil { nextMeta.AutoStandby = cloneAutoStandbyPolicy(req.AutoStandby) } @@ -113,9 +121,36 @@ func (m *manager) updateInstance(ctx context.Context, id string, req UpdateInsta return updated, nil } +func resolveExpirationUpdate(meta *metadata, req UpdateInstanceRequest, now time.Time) (*time.Time, bool, error) { + if req.TTL == nil && req.ExpiresAt == nil { + return nil, false, nil + } + if instanceExpired(&meta.StoredMetadata, now) { + return nil, false, fmt.Errorf("%w: expiration deadline has passed", ErrInstanceExpired) + } + if req.TTL != nil { + if *req.TTL == 0 { + return nil, true, nil + } + expiresAt := now.Add(*req.TTL) + return &expiresAt, true, nil + } + if err := validateExpiresAt(req.ExpiresAt, now); err != nil { + return nil, false, err + } + 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..800a4ddb3 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,109 @@ func TestManagerUpdateInstanceHealthCheckOnlyPublishesLifecycleUpdate(t *testing t.Fatal("timed out waiting for lifecycle update event") } } + +func TestManagerUpdateInstanceTTLIsRelativeToUpdate(t *testing.T) { + t.Parallel() + + m, _ := setupTestManager(t) + now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) + m.now = func() time.Time { return now } + id := "inst-update-ttl" + require.NoError(t, m.ensureDirectories(id)) + oldExpiration := now.Add(time.Hour) + require.NoError(t, m.saveMetadata(&metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: id, + CreatedAt: now.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.Equal(t, now.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, now.Add(ttl), *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..d6d065935 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. + 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==", + "H4sIAAAAAAAC/+y9+3IbN7Y3+ioonv1VpD0kRV1tK5X6jiI5jnaiWGPZnm/vMIcCu0ESo26gA6Ap0SlX", + "nb/mAabOE86TnMIC0I1uosmWbEnWxDVVE5ndjevCwrr+1h+diKcZZ4Qp2Tn8oyOjGUkx/HmkFI5m73mS", + "p+QN+T0nUumfM8EzIhQl8FLKc6ZGGVYz/a+YyEjQTFHOOoedc6xm6HpGBEFzaAXJGc+TGI0Jgu9I3Ol2", + "yA1Os4R0DjtbKVNbMVa40+2oRaZ/kkpQNu187HYEwTFnycJ0M8F5ojqHE5xI0q11e6abRlgi/UkPvina", + "G3OeEMw6H6HF33MqSNw5/NWfxm/Fy3z8dxIp3flRrviFwiweL855QqPF8mR/piy/gd4QzhVPsaIRkuYb", + "lMFHaIwliRFnCEeKzgmibMxzFqO3x+co4oyRSDcmh4yPJRFzEqOJ4ClSM4JmXCp4RwkcXSGFxwnpD1mn", + "W9sPwvSTeP0q/W1G1IyIwGCpRLYVNOECqRmViDL9NCJ9f8OUyMnyynY7NE7ISNGU8FwtL9SP/BolnE1h", + "Wq5dlOZSoRmeE/SBCI5+z3FCJwvKps2LNCYTLgj6cZGRFDOUJTgiElGFKFPczcasUUlj+2mIuOiUcUFG", + "MZGKMqzbH2VcmBNRHf1r+AMnyHsXhgbvIzXDylE54wpdEZJVJ4qv8VV1GX/d2em+GAwGv3U7VJHUHCt8", + "Q9M87Rwe7O/v7nc7KWXm39vF6ClTZEqEHr79BQuBF950JM9FREYRjcWqmUQJJUyh49OTN3ecQGd70If/", + "bT3vdDvbL3b62wfP4d/bBx1/WksLXx35x9VH70JhlctlHmRO08gSysgjkuVZ/5KnYyIQn6AoF4IwlSwQ", + "HCkStyC6yrQHoa2IOJvQaS7cEQwducpyzrBEmBmm0avxi7KxVucu0kws5tdsJEiKKdNrvDSIN+4R0icU", + "2UOkhxRxpgRPEs0UlCJppqQ7RV3NxhnCWZbQCFhP5VDtpQPZ6XZYniT6YW2E5W6ThE4pvNBqaaj0Nsl9", + "ixRHhCkiihPeZmkqbLGp43K5g7tR8sVWXc54Eo9ypmiy3Os7/bNdU78nKtGMJDHik0kX0QnCSLeifzY0", + "Xl32ncHOQW+w1xscvN1+djh4cTjY/59OtzPhIsWqc9iJsSI9vctt9gaYtqQsCu8Oq5NIqi8kQSKzO8WF", + "VdnAMYl4SpBueuXI9z595I3XzXG5qfpFZF8sb9bAVvdrV0Y76k6wVAUTgv2iajHCgTG9pSmRCqeZ5kN6", + "DN5iNnEh12B9H9zKr1zg7U9aYEZu1MiuUHA+IfogNxmJ9I3IHTcpBAzdniXvgmU9CI0LgqUesBaT9GX6", + "aydnMs/01U3iUZZgpdvVMhWQwSilUupPix9iKg0f6XYckY8YVyORM2ZeZERdc3Hlv2lbGdGs0+3MsBzN", + "p1ne6a66tqpEDV2QBGcS2rM7LkZECC46RjRejCZcuE3Sd265hCuaWlohWVyxgRXqdDuVBSjYuZuLG3ex", + "q8HBQS9AS8JoFUYNgMksD9xva3m4xdBWM3Zzixgh2m0zsh/LKgeIKZ4yLhWNZCs2D8KD3t6UxwHWeVI0", + "h2hMmKITSoSVqwkS+n5ICXKNIN0IogzlsnYOCtF/ROZaVxvN90YqypYXpabY+JvnySbljejdysX2Fydl", + "DZFW5x5UnOaYwpk8IXNqrpaq7Ga3ZhQLOiciwL4LAcCwQvMe2tBnXbMQxhnZrKwUm9OY4jbsIIYxjWiA", + "es6PT5F5jE5P0MaM3FQ72Xk2ft5pbpLhNEALP+YpZj19IPSwXPvwrt/2z3tBFYWnaT6aCp5nyy2fvj47", + "e4fgIWIg4fotPt8JSapZREc4jgWRMjx/99Af22AwGBzincPBoD8IjXJOWMxF45Kax+El3R7EZEWTrZbU", + "tr+0pL+8Pz05PULHXGRcgM629uD4y+PPyyeb6q6E6P/7nCbxMtWP9c+kYZ2IkFQqza++N68hQYwqh65n", + "XBIU4WhG0NgoLaCeQ3ttSL7o2N1eoRGcUJlx4PPo/VkpzYFqSG5IlKtKv9+imGpdOLJClTe5FiOKuN4j", + "ff+GJAtYAWTf0aq2cqLTnYWASBC8pjv9RqvOls9/bshrlMqm1t0rms2nNEmoJBFnsfT7oEwd7DVPxjvF", + "5tpc6uql/hmlREo8JWgDzFKgbhgOr6WtCaYJiTfbSdhNk/k7H3v3WuXMARn08Dja3tkNMrQUT8koplNr", + "V6zToP5dy8e6HYXg7fBEQMJoNw/oUpDJcn8/wH0CnQgyIYJoiv/E7jLB54Rhq1L9B/Tb+b+2SoPrlrW2", + "bsFinpevf+x2fs9JTkYZl9SMcIlN2CeajGCpEXwRHjM8WrXXHkVJhcXq8wFvfIaTWAqba9fGmn60vIWn", + "az95q9+pM3RgR4WA43GBRr79UktaAZGFM2Uf1EzAfIoSyowapPm32QsQ9hYZ+S7h083OZ1uHYvmXD78e", + "9x2Yl/mhoTX9rFtoBQmf+qs5I1ioMaksZsO9ahsqR9e4/OeV41O7QLEko9Uc5JwyRmKwuduDbd7UsnVQ", + "94FTdEXVaK5v4NCZg2H9RBWybzQ2lfDoakITMpphObNGyjimxuB6XplJQISsODMwGAlcgyDagFJ98ePR", + "zv4Bsh0E1tBaf/ULyzPxvtbNW/FCYTHGSRKkjWZyu/0dvUwhYQooDb5Nd09BgY4wDafr2N20ynsuZ+Yv", + "4N16VHD3aTagySvRfy8Rbrdz09Ot9uZYwKrr5r1h/dX15P32fdmp9+t50b/34xs7FO+nH9yovN+OywG6", + "VTFqUtAEDpMOS5WcMtWjDLZAC+DW8mSujtLMai0ZSG9jIeHBzf5JgtXxkkiFNt78cLy7u/tis71wReXV", + "SNIPZDQdr5Sb7aCtoKw/Q/ozfVVO6RSPF4rIPjrGjHGFxgRFM8ymJEZ4ovRXdqwVBXx/sM4J0ygkEdEk", + "IqkPe/jF85sbrF4c0Gv54kM6FtO/7wb9n2Bd1Pym2QKH7W7qV5HALLCJd7uoU3yjNX9DJSHn55lxV6Hi", + "JbsHsj6EPioV+gm90Wuu0HZlpYOLG9b+ChcW46yXM/p7DrudJXixrAMqgtMeUERoeQ3fMAdIrj8+Up8f", + "842b6TWmSp+cwmdqp9xFPIn1vTOhQqpOe2/YrQQDIj6XiFQ5ZWGJaZki6gvYeKkX42w8KwmdkGgRJWSZ", + "uTv2nYncmltjou8c+LMwILZl424oFU7sfjwvuqj8fFL2V/n9pen8Y7dzjDM8pgl1nLnKp61PfmQNf+t2", + "yW/sxHz6xn75sduZEKxyQQJreQHxAcjYiXsJmZME2bc18R6io/NTJHMxAZc5UKt5F5GbjEsih0yQKRZx", + "QiSQ+vWMRrPCXDnG0ZVEmJVhAegoucYLiTJBJGHqcMiGhYVcDjtdNDTKl/3bUIn524SGuJfYVBBp/0Fu", + "SDTsdHVbCZ/KYaePfuRS9bS6bGWpQzS09hg57KANa1LLsJRqJng+nQ1ZihmekpQw1UUQpAFmYM2ZksUm", + "wixGw47gkiiFeyTNE+D9urGjLEvIkF3QhEacoRRHry/st9dUzdAb85F3ecKM9WXdRZk1PUuckiHDxgyq", + "d3KBMsHHBCU4Z9GMSETYhIuIbPbRORE9t8RubxEWRG+GMeKinOkjQnC5GfLX3xBhSthIh/bMRc/kNvSn", + "l75QouVtvjw1X3wsvCO3+fgX+4lmVXbGQR1FLEprukfP5kYsTP3uOqp4ArpDBisKu0r1W9cMVbZskuAp", + "kIp3iAwxyiGLeJqComd7QZLMicBJsUP6juNgT9B3g72RhixKeB73ZouMiDmVXHT1DUEiCEYQXfQ7SfMu", + "dKr/6qU0EnyeIs6GLNFkvIXT+GBvs4+OUKLlnriYPzXEbUkMOAHYA6ge7bCDnU1+2DEToxLpi7+PXjIl", + "qKE5JA3Bjc1NWiOutpvnMauluw325zaNXZgvljwc5mdL0N0lFuvRTUmABRl7XDR0Z61gwMsyuFvX1c4o", + "O75ityLM7E5VqPMQUYVwlhEsJKJsyEpqYjFQqf0oEwSWQ1Jlty4lCm2kWAF7UTOshgw4xDcSVXZ/s49A", + "bjTxYSVHAkOvIMnCDIgMmRvzNU0SMBwiCGwwHM4cNLCMeREUcJgwK3hfUnq7rPPL+kUrE6gcEdP3X1+e", + "vUNyIRVJ0ZgyLBabyEWkaV5rSHPZMRcWGe3ugeYOolp11jGHqCf9LeKsKkDWz+taF4IVQUu6WEdhP/JQ", + "iCUW0SwURicVOj5/h/RjqkikibgyXizSg6AricuG5nSvGIRYs96V5oDprJ0z10cKRrxusqfFPRIWkQp3", + "/LK3C6w5pR+XJCTSvMrE0xj9jSDrXEY8BZZe20zLQpuWyDUu1/Uu0QaXW3rCm97Nos+0yFkfvWYIxAhU", + "kSKGzAZVRkkeE4k8fo5K862TLyphUAXZD9m//t//z8gSMZpTXEgb6D0VKscJ/WC02InAKdFMr3qlRTMS", + "XQ1ZXQxBulXJEXaXSrHMVPoXiqDTmUKMX/eH7NSMqRjwhuQTdY0FyTOtZqJez466Z+WsTRPEBQ1RVQvR", + "/LW2N97i3DKWsOLOKza0u0xh60j1l1JsqdLqFCtyjQMK8StNer05lRCyBjRh30Wn530Ej8GVEM3MU81F", + "tRSLNgxVUDYdMnhiRWJNXiDRGjKzrsk+ep1SVRA/48X1Ym+6IZthicaEMPAiJnMTgFyS6oLUN6CzPehv", + "Dwb9QX87dDSmeuwjxUfTPGh3dXddrIUZhd6f9RTvvT9DSuDJhEaajjIi7KjNBVMfdJCfpzwmScNKuw9d", + "CEXS10qGoPFUCzl6uazob34zl1NGhB7X26NzfcHB6n+Lhh2Glfmm5PS9TPA5jUmMfjl6q6/W6PXFpl01", + "q5qahrWEETSCdzsyHzOimsZvnkIMbngrIJR2HfM1S7S0RevI+9MkGn3xmwa+WSGTlBxMSydaiC1IsI9+", + "rsivVv50gStaBF2W6IsW3CH+FhWj/Q4EGpQSzCSEUGpZ3xcyqBwyJ31UBA4taSO7ovpTkTPDpcx92PO4", + "L4glRh4BiawI0p/r/+/NJY+ubIiH0TILTstwCqfbX7olyYqqVkLOWhOAp7AElrSQyHzBU+sIkuFMzrgC", + "rdr9Y0sQqbggm1o5t/FS+nmGc0nQX1BKUi4WQ+be75pzlmBlYhiKbyeaMXXQRpRwRnw7ggkgwEMmFc8y", + "ElvHxLdaUoPDjYsgQi4KW7V5SWvJ0IXcMhKARMUsYANKD0yyKDfZm8kmmBpgNsW09Jzz1Ax7xlWW5NOe", + "nWcHbSR0Tuy09QTpB2LaGOMk4Zz1bDykfrW0ncAjpLCYEmVNzxLaB5oZdoZsA84tMjSk1cuc0Qhuc3hv", + "muU938TRQRuvzt9tlTFLQ+Y9hk9iKq96lPcSmlIFlhIqr9Dp1msk9A0NP1M2NaOHl810iknC+3aKffQe", + "J7k9p0Dten21ImykfxhpT4lczSZ5cojmH6wQVt2PYicLqWh7t2stTQlYmBolmfdnSOI5cQQJX2mVWip9", + "jHA81zxMWmvsFdqw5NSzfi6gO4kYITFifMj8tvRBBjLVQ6yL/MgYYiSqLdKQlWJbLZLcXW3OZViYIpzg", + "095is1qjafAxfLrWcgsV+aJQ6msXSUabvapH56cmcBhH5TJ5+itBrzPC9Fsxj/KUMC0TTbh7tRpFNujv", + "NEWmNXR/4V8rRfdyxoVCU6oVYS3GcWa52bDTi6lQ+vjLfDKhN6BF5mAFMjKNMW52NYcadnJ2xfg1G3aq", + "w3x2gHcm8Xh37W64YXcraxjcB2B61irdmK+32od2vNJhpkVNLblrbu9p/pbCP4O7rExJ0pee6OmzklCI", + "3He03TXRulPCtKqqH01AY3cSWoPLpxcUZe/dq3QrT0zDjprI2cYNXTOHacLHcOG5iZRBp310Wl27LsLG", + "8kFlfZWLbAkvMBRtkP60r2+XLKK9wWAw6OGd3mDQG9SpPdnrmZD3DCtFhB7g//Mr7n046v3PoPfit/LP", + "Ub/321/+I6iQt4xWdX5tO88Nd5a6yA3WD2GtD3RdeOudPWv+8JsPL5gWGnc6EgSOAU7WjuE8l7Nj7/Vm", + "Wj8+XY56MysV8+iKiD7lWwkdCywWW2xK2c2hFuekqq7b6ndXGVgCyTFGMCrsDlgia2H5dWuOBcVM/WbG", + "iIa+cUDLbwhuDX3vn8CIUK/n2rHsy0XeVxwAffQLVwhbj7RW6Q7hpeV+pwKnKQZr5xwnNIbDYd1sksbE", + "GGcZPNNvaClJEJULhvYGA/dk5A1Kyw1cuqkMO9ZwKqIZsgYhgcAUYjJkIpJB9C2PcS1NybeTLJ00fcz+", + "slX8teH+7P/2l83//R+fldSB0FbQuDFn3JKf1eK8gUttJPyaiAhLghKiZyu7KKZTqqQR3mIIjQKD2rco", + "MsEeJnRRa3zMmajhvSo5p4sezmjPWl6Mu/tnwqZq1jk82A0u7ob9o/fbf7qfGhZW5ElIX3vDc7C8wmM/", + "8dqNoZXzxa1unoDTJaXs1Hy2vSxOftoOu4ms2mmj1DVutb5jiqygNQNZzsD/2O1EaUiImBMh9FE0fO34", + "7ESrMFfEMjStNaJhPhjsRvAC/EnsL1qI06oM/LZZlXhsDGFA2Pm1Q6IZhzDIJOG3MVJ260y9KUpwpY8s", + "tNol91+OKQQnunGLQ7C7fRGNBb8iAlwyeokpkeiKLIy5YFoxaBI2R3MsnGvwLaQlGKXVPgJvEAH1WCux", + "kJw/4xDgYdii1dlMGhTBScktU0yZsX32ZMQzLeMx+5qx6F8SNr9EKc6MWi+IMZqnWBFBtbpYGhZTElPN", + "o4eMwMFAGdZnPoq4iCEHmhtnerkK30h0adTES2j+kjJN1pfmYNZMpX90Xr97+/3rd7+cjF6fv/zl6HT0", + "08v/1j+bjzqHv/7RMWAaReDV9wQLItB//AHz/WiCZyGmr3OUqxkXVtt1LnqIV8EZ7fOMMEz7EU873c5/", + "+v/87eNvLtzTZO7N9TEIDOxjUMy0ts9ADL5LgJLIJtW4dE69ZJpFObODZ26omfKN5HerIwEqCuWjcSaD", + "uSl1u0Uph24PBmffb9nokX33j80+sq5bGL7mQVxY8VjONPkUuBzgT0sSHtkMjUlTCr7rKsTgwdeacRoK", + "Ea8xp/LVZR7V65VPb8GKtsaUbUm9Db3odusOdHPnQOWXbE4FZ6Ceg7g0ToisnpVfXp+8HL385X3nUF8E", + "cR7ZRKzz12/edg47u4PBoBMiUHKTUUFkMPDxaCx5kiuC4KUyxrSPznIJwZ2WeUxysIOCfCZRmqsc1CJy", + "EyW51HwKpAGlklD+8fPezvbb7eeHg8HhYPA/rQNWNemvYd6vzt+ZDHU47wQnajYC19y6D3+Ed4/hVWAV", + "YJsE1T6wSsWeOmulPr32G7Qxq0pXZnIICHLY2X31vTlQ26/gLDlCtKnARSum4drqvfo+tC6eNWpZ0iuN", + "bZDhxmJH9ctgNMZen0trJvI94T2GAWIHxAssYjTmWMTfVmNp4Mu/vjx7941EYK3AifUTGdEbvjGyJCcS", + "dGUX41MaW2W3tgRg+UmNKREzRMBTOqea5fTSlHL3XZUtWWNKxYba9xxMATueFy7U6Xb0zOx/3AQ73c78", + "QzWner050IbGtNIV1yiBOMkoIyu0wC9E2L/m4irhOAYr0eeU9b14txoMjXlQ3e5COXUEvoTuNMYsvqax", + "mo1ifs30kAM3pH2CipeLa/JGzwQn//rHP9+flQaR7VfjzN6Z2zv7n3hn1m5J3XQw16aYSJ6Fp/EuC0/i", + "/dm//vFPN5PHnYQRKe8kntv9f2laqAPUWNwuE43f4Hct5LAyP5c3+NLXYBiErlw+JyLBC+86sWPqbA+A", + "p9dGJahxN9rvPBPy6stFt+bEtVd1a9zOIHx9fLUdfeG2I0GA9Y6yAitv1QF5Y94u1foA0QVo7nssC99r", + "G0orCG1758z+ubNMcmGKk1c0M8EUIzwtcj9XwetdXNHMKs3whXPlG0Yf56BmjzlX/SEz8Dn6bALlkBsS", + "OQepQkfnp9JEB+grDS6NZUFIq+Ae7hK8bnyhImddNM6V1qu5IshaOGwevx4LvDwmKC8jNWtarp3gcvQB", + "LMsVEYwkI6PFypYrYz5C9qPGxYGpTrC0cF9C5Vl1vU5+OrtAGycLhlMaoZ9Mq2c8zhOCLoyotlldve6Q", + "ZQIwVHQnmhyp7ZdPEM9Vj096ShDihphCY4WzwyJJzF+dv7NYJHKzP2RviF5YwmIbVeokiiKslH2jjL+5", + "0qzff23Rm/BmnA+95eG6sK+Xp6u92a/bUSoJIcglRrZO6ISYZAXB05L6XP5cV6ter3gJeGAOZB+9k/pA", + "DuSwoxkU16x1zNXMV+EmlCSx9NWMEqeyfE0reWE9rlQZawrJ3izoHI6yvEq6O91G4MC5iUrQMkNFwQ2i", + "rNjsluVVNLiqvkHF3t8l0KKqIlG0tQGblgEFdW1YpDXlGom/tSnXMy4uGXWd5euPltkCq9s/ZW4gK03Z", + "pfHrE/q6MI0s5ReYn7tuZndYpdNiTWoG8M+zPEfSMxa2Aiw1Eoy55iXauMQZ7Vs67kc8veyiy/+s/KB5", + "nFP8tZx8jcxqAN9k+ie/fbkU0rvGgHnnsF4s774fR7IRbgrNt5ESmEkTYD7DGemjH+GyQoqkWgQDfD4/", + "po7x628RN8K5+3TI9NBkNcjQmbElnTLKppsQLT4mCMexsXVbU5ULPJEhnF9ngFqSgM3oLFoPoOraMHN0", + "6WzOl1WeuGyRXjbYWBP1kqZulgQ0dLBhqK00V7p7PeFCoOa5MuhddupVZLWa3XtdRIwdS2GDu8P+XxTs", + "og6ePA+o6pALMikdFZ7HpMkxYQWysNPkiixMgoB1kOAlF4nvGwl7MGwstxUvfO/KGOChuY3vLBwrxkVi", + "vSIQdVsL+g75C9ZthV6v1stfVXmDCSg9N9mSYqwW64A5HRfSkzP9gbVNElh8UKEPTT7TZdfo/AQ0LMQ0", + "sSQuKL3ePMTjQvTepf2lb1u7LLLjPgfYdpn24G8t8nbWibcmhNmKR4rE3apscEVIJtdPysRlzhr8fYJc", + "C+oYmUN2bCmG2nSR1CpDn2YAeek1tiISqm0Ty+mhZn29MTtMX2wDzGDvzX6A46cC9VtHvI9r2qmBTKl2", + "eYmT5BJt2Jc2kSB/BzhUu1eMs5LY3x6fOxIo4qzen3U1RWoucDlTKhvp/5MjfYov643Zb90JL9HInw9A", + "j9zb260mK5gB15qt2omDMDDNW3Oey9k9BS41WKMd7LuxSLsbKMvlDG0oPNVHx+L96Hti81Mt1ZRJEuWC", + "rD/rRyAeWcwZGFOCKev9+PbtORJkSiXk2Aaj+U1MeAM4mVXgyzgtN13Fq+bKnd29/YNnz1/04yvRJ5Ho", + "57JHsFS97T5O8QfO8LXUYtdWusBZdjjfXsvnzRYU42tm+U7bbCYFnmqW7CJx22iux+UnpafsirK4bQM/", + "6XfXR3s6xfq+/Q2ZIL08mwoMcJef09tw54AeWM3mXV1TAmR1JG8uFU997NuN1QG81cWa86QXY4VvE79r", + "hrscqpsuTFNGJQ+a+Zrioi/qwdAVwJpgIYBPja5yYwltSxOYrjEkkHikeCBv1ckOpyeAvW3fbYUaCti/", + "io/mE8rXZbdDzEglZ9VIJda8oZvoZRG11jOX8AHCqlkE0B3en/lBJf0h64EUdohOig6KZosmMagYODbu", + "7Q0uKomz4EQeLzYRRu/P+uhtMdpvJLLeYosuXKRM5uBJA6GoZ0QyfwA5ZJhQVf/cmgoNEPEmxM5w+6xf", + "lCsBo2Rh1AIb1pjW5mMSmmGj7F3iY660NNKuguV6Y+6jVdBclVCI/d5gu7e9/3Z7cNtQiHvAWg61dVTl", + "LTbo3ec+x+9OT3asKXLzzmhcnx2NOczKTspofbSRS+JlonIRjNH3QuEbYvDvHFp/T5HyJe7kqnfNSrzV", + "b94HzHQICMsiVd4eCLrOMNeijXqTWzboLDIwP5SnxBPkbVZFRIMpxydUXn0vCL4C+PzlexsAGEbmPgsH", + "2gFKxngBqFBg5BKcq4lEBXhKafze3nu293z3YO/5YBAAMl4meB7REeTNtBrA6+NTlOBFgem3Af77GI0T", + "Pq4S+v7uwfNngxfbO23HYTzW7dah0L/dV2jDrshfXIkz96QyqJ2dZwe7u7uDg4OdvVajsm6DVoNyLoaK", + "SPJs99ne9vOdvVarENLrXjpg6boAH6pycGQKA0Hep8xIRCc0QgDIhvQHkDI/o4wUztnqmRzj2NWCCN8d", + "CtNErgxrM53ZN429Nc0TRbOEmGewIa1cEjDzE2gpWFyLsaL2xu1asnDcawOe3FyKV0Ccick4n04t1l2x", + "dGc2jb0UnihJ4kNUqGwr+RzsZjmw35rowM6hJTX8rFUnizrnEYG5ugxEgyCooBOzaZVZuVgHyrI8SBKN", + "S/lDLgz6BDSK8JjnynjVbbGUshMD5MLAy5yzuJ254wcurtYmNuibuKgJ09Jg4DLcTQZ8LbfdF/oK77eJ", + "EbDPJXpjvjCGwvLnLK8WxOtCT9agyFxaflzYjW0zbaXLsNwCNnMXLmP6K3nnA4Xy9SYm+unzathiSqAW", + "klorsWhKMfFIF/B66zwp/eFaQ0qLdWfk+iEWHTL9eppse5Lh7H5WfFXEcGFr8PL0uY2z7yM4XRDk5xL0", + "ayftwkFN2CXvD5lNNPLwCsCRBlgCJkx8RqhAXNAprXZctbM+YMzubSjTEdedqdP/sCkGTxZuwBAPMEjL", + "ExMCawqA+ECvdk863c5FUTTKMqbq0rwpCm8trUgZWb+MuXP+7raRt5ngExoCwoFIIPvUKmouJvXnvcFF", + "b/uvJmpekx9IbJSZ6KGUx7UaT/b9dhfRq/N3501jKqouIX90S3NaAe9TRrsUKK7G1Wh91VahcadB3zNF", + "J6Uo/iIk2gKKxzifTIgYpQFb2w/6OTIvFCgEZ99XxVstRrdVos8rmwNa9ARHlE03W69+wD5Xm8Y6mL9X", + "5+/e2Co+TQDIequKSj8G+7iPfinqXKFX5+9kCXLUDxju6kB6DXld57OFpBFOTIumSAVlvr0NiLO1wHxe", + "fmgtkwGxOVwezR0EtDGfZjkcw4s3vdPX77fSmMy7lTFBXN2MJ0SPe9PjFnMHLlAmoVWYxLzJ8GEIQ7Y9", + "QN5aFSe49SJ55zWwOoornIxkwkMhPG/1QwQP0cb7H0x2sB5BF2WVrdS/+8g/Pn0fBE8MAMo3dHsBHdYt", + "qJUDHlQl64hkner0Kp2GjorJbFoWeZZrE/Kr6kbzq/X18Ewjzf0eu+Srmo3bypXI5GgZ+EQXlYDMp8aI", + "bS0lkmRYYEWSRS08sopvTpYdzOSGRLfI/nqpX/9oalrkgozUTBA540k1LGG3u1wXVUII8JzYUlBmTp4d", + "XnGUYnEFF6OTq1HOzApUI8h318G9zJTKbjGpH9++PTfKtiJijpN6DoJccrifkAQv0Jioa0KYmwqWCPsh", + "qPUcTtlQjkeoUUYE5dU17OwG+r0wYcloKnBEkPnKVVW2WyLh1my7lLaXAIBgFBEpG/Z3e9X+2k8nedJu", + "j0PD2l5bhDy6zQa/PT53ZWWKsr1umXeWV/mciJ45cq5+7+qt3ZGrCxy5rpjBhV0SGMYEKh7ZxA8/L9OF", + "G0FFJ/15JRfSYw7Sz96z/cApMEvVNef8t1bCXv24h/zqKWZxqPyxifc3mfJTgM+CoGORgyuJxiYWySTv", + "G7HcT1sQBMeUESlracZRLpJOt9Ob2Fkdbm0lPMIJIAju7W4/31od1bkynNdGL41iukrdczFOJgrGJbUa", + "+DWYdJUktnCWtTCImXVccz8Ae1oOH4SSx/pu8yQ8F08+GCxlTN/gSLkibWAhq3hAsX9sAee3Mh9oMDV1", + "V7So/cI/n4OgSzrDalYl/60l2ocwFQgx1DRirA+1dTRE/iEoUXERqsTFhbIJeGPiYhCL+9BF+DlE24qv", + "bPDcn+XB/v7u/jo+BMymdsztuQtM1bxdTTEm3nLb82sbgGipqszhjvTqYqJ6XdbQlOaISyS1ekF5Rtit", + "1nN/b3fnduvZdiKnLkqrxpdCCC3HZydGJoo4U5gyIlBKFI6xwlUmA6YlzWWg1AsmKWTpTL5dzVoawhl8", + "yJW71qn6XM7whpJ1bxxCc4oZnWiGbN/0e5YzvLN/cGgKa8Zksrd/0O/3bwtE8bJEnmi1FVsmZs7DpOjL", + "2aftwz3gTbSZyx+d86O3P2pGlkthLq0tOabs0Pt38c/yAfxh/jmmLIxT0aYWK50s1WCthoflFgiYxIeo", + "LLft5J5W9frDtmEIFgZwnCDqWyVo8v7g3Qoap1UU/1uk6q5IXdXiymuWLBrX5s5lVcuC48orp+onf7Uo", + "rUo/rHZ3O3MXvGP7NHjQRdXZZUf3neoGy5WlFZcqb2WEFcUUk8T8FXEGOLuhyoqVK9I9a1GYC64RW4Gr", + "6NL/sejd+/HYH4j3uyvs5f1kSyz+dssIlZUC6d+W5dD1XMiJo2sOc9j2WNwKbcvZWli4YGj2I9+Fd4ni", + "qvb+evpfv/8fef7s79u///z+/X/PX/3XyS/0v98n568/CXJkNSLgo8L6fTYkP1Oi3Yfza0tKZ1hFARud", + "Vv8aVtg+MRYHFc2gACcak8Mh66GfqSLClHOr5SIOO2iDgKYEX2lxF2rVmDSwTf3xuXEw6o//cGLwx3ob", + "sc0RF3ZDCugPmY9jnmLKNodsyGxbyE1Egl6g/4pRhDNTk40ypPXfBRoLKKBnPT5l5130B86yj5tDZovm", + "G3DrDEMNskmRhMWcv9aOykSl2tdJgQJhnC5DVtzWBSSecfv1S/x8SpJ6Ck/DoqzW36zm9HwQAg+E9BK9", + "kVAjBlSQgrI1GRV5L+j5YHNZn1ujYxQ0tIL8rF/c5B8e5SFzcVPO4gmJaQR8xaXtzWxiZ5ExaSjNGvEy", + "wW8WsDdvTC5ZjHCuZpoXRTbPPeL8ipIubGkXTQQUeOiaL417fcaz3njRm/GswDzAwgSfYOOgrirZ/6dn", + "J9p7TwSd2J6CqeuaRAJCJxwZOzOTAVhYF5Ym9taU12Ba9JkT+7qp0yJNaQ4TWa1ywVz1CAI1JAFco6A+", + "EpLJv0VRQsHqJGc8T2I0A/w7pZsJQdh1BkUGCx5HMZnU/12NMNjZPwAN1v17d6d1AqlZulVUlicBnTZ1", + "rK8FxzZsEgZgxIORM4SviQnSN6AtZAV2CsXhvxfINVSeuIKRGO+USWmTthhBIr1kts1g1pE9BhbBY4Tt", + "aWpzHy2dwkoSU4sWTLAAfJa0wA55aZIn3/58gRQRqUtn34j07sApMUARPSplbutiHR2fvdzsd4K4RxVX", + "FWzVyiSn6qAD0Ac2eKApJqK00eCUdNHpCSSv2mul1MUg2+AHLlBibsXyMjoE7IyquQebinqnJ1YATRZl", + "BIIRW4adTddiVr/eDtGbQgXExVCKNMSStlyT5WUCzdp4NJMKsdR6LWsV/GNW/bP3MSQ+QLlBw4sBu7Hx", + "/mpvc3RIUPqiqlnIbn0h+UEhjfYvb+8/N8rx55fRd28no1sf8CibYRmi7pnv1YSXlvbddyNX2b1oDuGp", + "9DuSNHi2/uaKrnjXkCL6nqt8HoL13O9tb7/d3ru9+e62ALVVZCoPta7AqG0PLnsfIK0ByFWqRo2x3kg/", + "tpHdzi7y/gzNsGTfKHhYs45s7z5rY5SAXttGSfvx0XxihlRwKQdzVUT3GsCvK5okRoCRdMpwgl6gjYvT", + "Vz+d/vzzJuqh16/P6lux6ovg/twBqxZuAVhHk/AVQDqqZOjfGaB2reWwBWCtY3Svzt+BXx/LkQuTbM4M", + "xGV2LbmhUsllDLNW0cafApBrPm1XUt9N0rRRVtZfjbL7YwUGNghKt3kP8LguVHxpOR8BOfYxU/K+PNTa", + "lTiznwoWa80I94QV23hjhXBWa/gD+02X191RX+9lOBX4lhDb8gUYly99Z5jVbocGckWPpL5XSIxOz8u6", + "SqWvwTVfm9OLnf72wXMoDbo9aMPYUxyt6Pvs6Lh954Mdc8sc4vFhFB+CPn5Xl5QlbKNh4OQaL6C0nlna", + "Ycfch57y6h1bqye2Cp9ZRrO9G3htXUprgKcFadXFJclRurKyR4tkwDpEWZpbAMaUJgmVJOIsllUReIYl", + "kpnBHTUVLgoBfchggF1UFBoGIQThKBJ5aVm0wrMV5/PM0j1U2cw40yI+wOz/RBYSpRR8nEX3ENkoUZFz", + "Eg/ZhnD5SUUiEhTYjPUPEO3fdVHlemhUQSUP/cGQyVmuNBPb7KNjzmSeEmGNrmhMwSG0iWRuNFYYL6zG", + "QjNMSWMihky/FkA2/aPQPg4PBoPBoNspFLVd/e9BiJru1bfZt8i9JsMWsPWYxfAF0D6RM5SzmIii2jYx", + "5FCPgLulX/QTIXvd5+3EK/t5KVeFD+Y6hN920L2fipcKQ21QvyH48w669/7dRfRWGT5OfrW5Pfar0W0C", + "FAiKeJ7EWqEb69vO2NtIbK2MkijDnc27VKJ3ptJldeo2slhx9HtOxAK9PzurRDUIMtE8oN3EgUs07APP", + "brUNO2tMIGtHcxvvsYcu+xCIsnVJxZMQPzt+rO9AdCnHhkIrdquKXrzKfKa1ymCSCGVmnzTRrJhgzVIf", + "k/koz0NaiX7kUEfevTs9qVAKxgfbzwfPX/Sej7cPenvxYLuHt3cPejv7eDDZjZ7tbu/srkgoaZEkdve8", + "r6BqGogHLqK/Ry4KPRQc3JQDUBMCbFzzNWUxv67cM8FAU793G8S6rvvlEPXWQwgmtiRYKmMnaGAZZ3Cb", + "kki3bQK7bSJiUUEobDA8eDvY/lQzCwyugRm/FTkz7kmTOl+Y4lNvwP5mVcd5N94KA3IJJOtWy++8/aIN", + "DvdfHO5/6qK5JIh1Y6yT0wNublNklYMWrmVZuEw/z2DjDH0dK3wY67hNyuh0O0XeCPwNt24tJrl43CoZ", + "qunAdsNsZBX/bsgJPq0oBhDxYaDl4kMtEhTppONcoSLzXMsaxwnPY+QZvQzSFvizTj0lQTcD7iVrCzPI", + "mSapQSsTANUMBREo04wY/Hi6EZtAfIhewbvwCKdGf7KDMGU5fBcWjhcm7kSfL9e10WZWD/nCKjLwjdZq", + "kP4XTFsvg7WNrm7CiEGH6BcO3xRqFeN1I6t5HfSZ5dfrBtkNm9nrACGgMyvTHaIfCjmukASt5Lchif1z", + "ZBlWicOyWcmGtzve0dRS7pyX2d3tmBXtdDtuoSADfDkX/F1J9UvnzyfFUEAWwQmc5TLXNlc0sejTMBMq", + "FY2kTcLQm9skX9iKQSQeGS2lKbbTJHBaTab4yIkv78/QBgAM/gVZC7L+12YRB1q563Ze7L04eLbz4qAV", + "jFA5wPUy6DGkFy8Pbq1AGmX5yBohmqZ+fP7OGBkio74XMSTvz3zUhkxwzXr0zF2Dfucv+i989KSY5+PE", + "c9BZqDUD1gobFgQIK3hRQzzh7zSZ08mE/f4hutr5u6Dp9s2B3BlvN6DCmo7C9q1T30m/ZAwm456p/hMG", + "uAGCErIRA+oNkTADdEEUAvrpIRyBHlFkBVuSc0hRdsWDhLW3u7v7/Nn+Tiu6sqPzDs4IrF2BS9mOwDti", + "8CbaeHNxgbY8gjNtOqgEAO5mVscMnzNkS/cOqgJpf3uwG6KShou7pBrb9jxtXPL3Vk+zk7KLDsnNhQ63", + "dMqDq727O3i2t/98v90xtnbYkbhZzWFc6o9ZHosv7+/8BkiTb4/OESTWTnBUNaK4SKtbjUrdalRQG8Fg", + "mt9iYM+fHezv7e5stwMzCwVvWJi+yoGt8q7AoQsQRWA3AkuxzHq7TbdFSJwyBPaGRAmm6VHkUhVqt4/B", + "Lh8J81q5CW0uBmv6X7q4WnzbyopU2IZMoosRDbhAOSsqZvTX+z4/iwuzmWub62E9Vw+ltzC9ehZ1x1QG", + "u8NSZoLMKc/lZ2iIK5N7Okk4F7f6tklheUNknijjZ6QSvT/7BniKpjUkFcmqOpSlxhXYRHec3K3Oc4VE", + "wkTetFitdqPN1q+acLfh1HZXAVNUuEEjIlisOVfO1gdRHuMkyqFGDC72U88KwHwgtT7LkoWJkU8SzhmK", + "ZpiBN0LYilRsijCa8STuByNK9ZPRJBi+wK9Rwg2W8RUhmS2fYgahP9MiDJ0TtOEXDjOkVCvnuZ8aJmML", + "ZFSpcT8N1yXEMpT0VaSU6/XEinswu+aTiskx4VMJSqGC6P9+Hd09w8IE9WNmygHNU6NLBgKXA0OsMfPQ", + "jWpuUj6xCq4VOSBh26wkjgSXEpGETqH0zPuzWh7wityxIht4fWBkdbAtSNd4DgNXGVxxsnXVsND9GMiL", + "+ZQbEmgYcu9WhBw642SKWQ4FVTxCthbvfuuwwhmXalTgO91ysFKNoEpCLkgJAldkrxf2IPdO8F50rO0u", + "y2Xjd+/09RJVhZtqGmAzTw2uaHi1ugUNhsh4GeFqJahWidJVh2S6DQZbCatPJbRKPfgvtAG5Ix5b8qDh", + "N9tEo4RVVt3PkrZqi17+vDe4aAuPthoN7Ryr2Smb8ACGxi1clM4SbaM+MyJSCnVCUEwYJbHTJQtfpTV1", + "QeJ1IgmKc2JXzsinAtsFx+Z4Aw4GczYyyqY1Xl/vsI152IxhdREF6Ne+2CauSIYTU9+KHNbKBAZKhMsU", + "1VbRllSOwu6s5YYFmeYJFqgOAbhiyHKRJpRdtWldLtIxT2iE9Ad1B/SEJwm/HulH8juYy2ar2ekPRk0V", + "dy7M4Gx+ndmQWr/lFL7Ts9ysZfeCJWbLfL8FyCttwrSCEdc/0IRYlLx3jN54hF5FGd/bGTRlnTc0Wsk3", + "X0ZYvC3ntiQbPPG5DOTorZRyXK0gElvodyP2ZLk0ZUta3EoOW9S5AO/m0akmYHwaxMex4dc1gA80JpA/", + "46a2zDVasMU2UwlWTMjlDP2dj6sG0bbxtYE6XBushJoQZBKMk4cdXWmQNm8srYm3u7fBcgC2qicKH90S", + "ImFdxbIykKmJn7xZKt41I3bJqJujKeTVojCFC7QoYABsr+3xAOpl1gKBwYD2ItUCCpZClZiFVztQojEX", + "AoCVtYTDmZsNwJdomUevtcOPQm9nZIEESTFlQ0ZZYSQFUDKCGJkT4WWbcqGVrCmJ++hvnooHUNRpphYW", + "4xyM599IxK9ZMcYh8wepG8+lbueIGcuiyKEGfvmSbha0Pk0okP0LTjAlqIO3nQgiZ/7cQ6UgtYx3zUXc", + "WGNngdwrULoFfKxI8SvCfFZWNBNUDU1DI/PVcricqeMKT63+iSqlVVG9dOrq/nJJRFhILKZUvNIqdMU7", + "Kp5yYsBUAFkESubZvwyLL9BEWmCHlM3/1TVZ/nReNF79rfaahw/i4HqPjNk2aIKNTL5MLdin6klbG6oC", + "6WSrUGGWfQlow8Uqu7ojVUnAq//R6p5sl9FWj8p3o9mSJKr2vvd8/9lBywIsn+SsMyhYn9s1N09XuOQa", + "duqsjd/n+f7zFy929/Zf7NzKw+ISOBr2pymJw98ftEFulD6syb/+8c/3ZzWvzz4EOw9uNSiTwhEeUkMa", + "R3VA78/+9Y9/ulHdeUAhRrOMtN3gt2+M0kn8nXSBAlUXXjsn2Qr9/qhiJMAFm0EbZDIhYAYdmXXrlYOp", + "wWm0k4JxhiOqFgFGjq9NWHnxSg0xuo07qDrYkMhr2rbooppzyXxcJm9uuM7RfxrfcI0Wnreu4yTzcZMf", + "+nW9V+OFLr0WfoxDixADWZQQXzZwF/O5xrISOa3/jiDBwaVyLae1mDdWo9fWcw4gisWWK/NCAUOo5zV5", + "0n7kb39tOz2/ZcWsU1/x31acw+YjeCurb+BGDhh9o/U5rDX+YC/Au301GvsV1laWsKuUYytv3dv32yJN", + "d7neQHGD3b4/LzPxNh/WsXWBHu0Y7JKXbXcrJNFATV7SScCAxhPSKwL1bEYKkrnxCOozb+HaA6mS0RWf", + "TKqYsfvNGOMAnwNZVa4XrJTWTLqI3DibRR2g2mDlDDv7ctjRKsCws50OOzW3VTBPMcU3I9tBFSNlsAr0", + "u0wjrw1SuhmMEx5dmeJhUJO6jwYoJZhJlDM4/DWv2vZgtXeo28m8vSkgtokJcVpiWzCmMZnhOYXKDtan", + "Mq0EYpIbqiQEjEI7hyjmBjWpUjnVzlC/ZrIID8tJw6WD2cI2rBvU73HmIlrLd8HAN4F6rewDEbxrk/41", + "x379+qxrAhgg9NAMrBLf6CZqRqAZZNFFrUxB+Xs4fnickBGMuw57ny6vo5/8DZ5VQSRR0uJgl+RQIwIU", + "8ZypOh5+2k6Rq+ZvLV9JOYNgPxv+Afhmtndbdj8mEZxIuXwWq4R+B+Ku5Q3YlQ4lDuyGSBgOBfiSwr7i", + "N9YhXB+AMTZ4RY9NO35ct/ESjqTitkpWcapH5CYiJK4DZ4ZfaRsrb78Mxsr/jC3WTlGP2L4N8c7Ls+vf", + "XyYVjLVptf2YfsZZD1A+3JZaRA4DsWcxX6qEVoHw9rAjRiGY0tALbVKbyc3qtf6F3CjAGY/zxIDHhUnX", + "sip7Ga1b8TunEDYdaC7Wl++/h2psJt78TvXYbKj6Y5Rks2/dSxm2pd25IMq9e2HJqLkYf6VgSsWl5QL+", + "3SvVGBtDSl1kL3i0nW7WSHBvFraKWHDblsmQDKdklAkyoTcriMe8YBTjKn5IeZCKDAaD07mR4hu09wxF", + "MyxkbeyMTmcqWVQDcPYCmESfVKtQEEWYMxS22flyN92Hy9Fudjv91kPC8YWHwbNUGsSKpKNV+NPHpbfN", + "WuczvAArTqOT8Nnu3mCwuzO4EwC1G9Ytluu4/MRW9qu205RS531nHf2VKFW/hSKbeblc7LWgkBRdLJNU", + "guD0EBJvMhwRlJAJgM0VZbrXexbrXa8evBWoLPJLQf9uo+y+OR98tfRM0ZXF7nbT6DjnYhXsx3++xiHa", + "wGaiJWi6QM7dbm9w8HZ793D/4HB7+z5Ao4tFasr2ePZh+/pZsoMne8nzxbPft2fPpjvpblAPu6Kmwk4b", + "Wv1Jv9sYZVNeklXQoApLQxt2DhkR9TrA9frZkiSUkZ4sMqTWpymu4AXG/772/N/Ozm9msFJ2uKhO0hch", + "sCoXp0JZjwN0ZSez0ndRn83pyepZ3CkDqT6QML3VhwLk1W4wUOlhu/OJEAg5a3kNvfNebH0RrcyKW3cV", + "hTzscNKDu9yw4iHyriEgeLNedYEvX3IB2+mUC6pm6erbonitgOOGuOkPUsVVYKU+Op0yKALu/1yEyflK", + "lP640+0kH/aqZ8b+3h5iyyL5FgRot9qXClqEkUGN+dWrAK+UiocwkexaV9dj/m67t/0C4hCSD3vfDXov", + "qhEHXbNa/vJtu7crvw7arKFfSs+VYNp+cauIa7eeqyjoJxoqBFfeyxbj19J4WXLZXR0u4bayweXjpT2u", + "QeY0CqCfKunZy23kC00xSfAihPHuGWplTXv0iQyNyZQy2cZuuzsoDLf76bDTR0cWaBt02bLAfqV5KK3u", + "0QlNUxJTLWMa1b85g2GnpS2urkvcrsaH+yogrfXD4tqL9RAJ6xKu1l2T/U/Ix/0k7bedxrsKvQPsak5F", + "BbAueLGL6ARhViv0SdkcJzS2ifSQGAnxaocOEa0kWcsDZCkHOjtJF025QmUKfUt7W86a7YLF+MkN2FtX", + "YGYYgtj5LIAoBVIXXcW+Tk9QJnicR2X+aAKDLhE/RF7DQlsh5K8Pyb1P+wYkZk+4QOvtG00GjXb2yab9", + "rtkmNcE2b/X2YP1W34tRpNvJs3g9DzMvteNgt0JAX5OCGDDRVJe9Jgl6k/mtBUd/46/gss5rbMmRFony", + "zDlYNE0tU1LA3QIuhlBc7wlJiL6mlhtBPInLLAkqSy66nqVuHzyfNbk4wSO1PJCfCMm0rgL4R9Bfitki", + "ODBXvrO4SzYGDjVbGodXz5T9satVHdyztZJY41b5JtymUgSGy9ds3gYv5cozfxdg2r5otoyA4hh+RUh7", + "0wylb790YW+N9uP7MMs9ppD22roeavioDr23gCF3/ZexwFqsqxLvXsg9HyKLt1YzbsJ8rWeB+lbno97/", + "GCszGvUPt777y//d++0/g9bmmt4siejFZAKBRldk0TNFfLSO3q8inkIFAS1MTy2pEJyCDQnQxO1h9Me7", + "PyiYxuIXnC5NASK0vAo822sn9Jf/aI5v8pbxHfDJtST7yQU27qMQqeLuOtpIiZi6WHKXSLbZHzLITbsi", + "C4m8ul5WpHGE+o0sPvEi0NGlEQP7hM0v0ZhCoUQ5ZFqrxVFEMq1N2FIx1FT75sB9BMGJ346tL+YSv61D", + "0sQTEPT+bAku9/W7t9+/fvfLyej1+ctfjk5HP738bwjxuO6ZHuKepr29/QNb49tfye1gnYnbl0voozMb", + "pm9d/ZMcFFrA6ZIozVUOQSHkJkpySefOQaiSu1dOWE7WvXslgk+E2lUqCUUlWEjohE4I+PXhOrFBNVQ6", + "YqQSiqNb4wZlaPnGNoQz7AAn9Wrbh8pS6K0Ir3a5sdVFP5i1Y6EGhDRw2CHjFarYB7QXKgGvwsV+eC+j", + "DcgccRVcXeLs5u1AUY+KBoORh5+5UM/gxecopvluZfXMOU96Wr1pKEkQtCabtQhGzkNTJiOh0+R0mI4D", + "Mrw17U7pFAf8DCF/wmcpeukGtDZjamn/G6t/hfMYTur1GsyxNEtVqy9QMxJI1WtOc0i1VDsqK/tXg2dy", + "ZnNXqRdbV01UTZnassVpQ3gZMQfU8FXZyuUpc+iIPfhofRLuSr3Km5k3kua9OXPqQ03BWbFA53pprmdE", + "EG8j4IMSB/+WS2bzclqgsJjifhkRZcyqS+rRUim4myXaKCw/bgmKbONlc/jqOgdn+KboAVwpWC75H2Ee", + "ZRml7VffAyb9G1c6kk5cEzCMmnIXRmCvUtGqNXFUtbwZPlUtz9u8Hzx4llet4H5NZ6tGnGUfFdIM0ePf", + "MFU/cAHqYDPmyb0DucPlHxMBGHB1mPZWGOc0JfGI52r1+beV6e2VX5QXLcvTOtUXAxFHlXTeJl7gUDnK", + "MSyvtF4OEuWCqsWFXi8bzA1pkK4mLCwkdAQ/lx1DHc6PH8FoPAkkjLwijAgaQZVTfR5TzEBjQu/PvGJ3", + "pu7hEl4riECvj0+tucFB/oL6SBWQnou7PDo/7XQ7cyKMyt0Z9Hf7AzjMGWE4o53Dzm5/uz/ogFY1gylu", + "QWV6mz9t840LxfU0tpLQ9+4l/aXAKVHwxa8BJACIO7SvgwqCp54SmWEqrBaZJYBQYAiG6q8B199dqIfm", + "Vu6aZW9tM4U0Y8h+Idlru7m/gaAMZwemuTMYWGBzZa9fyN0xCQNbf7fRo2W/raQ6u0QBmPslNc/JlsXS", + "f+x29gbbtxrTqqHA2Q11/I5hm8RLQDvfv+VC3KnTU2bS8myStQ2H8k8cEJJ/1n79Te+ZzNMUi4VbMH+1", + "Mi6bBGMiEXbvGj1OSRRpVgHFeProNSPmOcIKYRO5LHIGJYrdh5pCq6fAtO02uQAp+p7Hi8+2hJU+nI3i", + "Y5Wd6ePycYmePx/tFGS8vJH2kUPYNlT7AAT0PS7qaz/aSdkbvLj/To85myQ0UqhXELCNR6YSQn4SwAt3", + "2ENcoN9zrjAqwvmf0JG2Muu4ILdueRVt/UHjj+Z4JyRkBj8nIsXMJEeYd9Yc+qXjbFwS5XFeeas5wofS", + "HnBTORAec1GBIFc9ov61VRcGl6+jvQACg+3TTC9+RMLfe4ATbidblFh9zCMHlS9RLslTOk7WxTYuhZCg", + "LPeKqC+F5gcPeWXZIgJ/wlP0VAj4FSkkvHK3li6FrUzkzCjAQQnwTZmwaL/7pir8vS2feFEy4NfQTUM5", + "C2X8qjhe9JFbU6P0qwVALAkC84yXr5VzPbwv5YTtPMQJgxkXnqKv19TXa2rVKTfU4qYAB9M75S1sELey", + "QPz57A+3tj58tT20tz20sjwwcm2tC3/n4z6yEakRjwmSM54nMRoTZPCOXOyJwqI//YCwiGZ0TgDUDoq0", + "5YmiGRYQWZKiGCtsfOiNhomVZomiuS3dXM/FIZYLXMexkGQEOHyjJvzJMgKRMkZipD+x0H0lnOBS3W5z", + "9oMG9qLB8mpE1zMuSYHnx5R3m0N6szTaMTTbH7K3FuhVLyAEUzteI0kCcLUr7D+cITxk9oNvHQtxgWAS", + "pyXnwgIwA6lBpjTbspzapkc6khEPYe28JQwz1ZMZieiERnZaV2Rh4zmDDbaqu6QH7Mb5/qxI2EA7m2G8", + "NoBnDIPznhTPkKWkqv+GQRB0lORx6eRyEEJYjHGSBAtzTBM+xsnIrM8VCfgEX8EbdlH8+v7Om8R4TEyt", + "9myhZpyZv/NxzlRu/h4Lfi2JGHY2+0MGiRh2rUncLQVEdA2F3NKM63MmeGr63DJD3Prjiiw+9ofsKE4p", + "cxQBn+BEckRu4DuobwWYGYZ7NdCDOU1hP/hxLhVPfeRTR3dmmDxXWa5sRokkqhtC/RwyxdEfDtvx49Yf", + "ZY8fwVlMcKzpxHvFTAlk66ZRyxHWsx/BqwF3O4EFGHb0RWrCPKYCM2VgOwtwSjT1t3SjqI6gD+lmfYUj", + "zFDGM1NZAohqhjXJVdoArAacJEjBUXLfasEddrJhPhZ6Lx034u4ZoLTaMaIMnX3vHabB3vPweZIkEiQU", + "UfJfF69/QXAr6z0wr5XhWialg2mBAcU5uE4dT3uJoxkyjiooJjjs0HjYKdy58SaMNZc2XKbXA5/id3po", + "35luujT+rt/XTRl35SH69Q/TyqE+S1lqcECHnY9d5D2YUjXLx8Wz38IL2gRfdlFhBGjDXHObwEkwBaQZ", + "78Y3VyRmMeL2FkgWCKOSA/mBK2PKsFisSiQMLL1dQT4xkYzeYvwxhMjFYedw6GIXh53usEPYHH6zAY7D", + "zsfwClivZXPlOrjPCudmQUQHg8HmeiRsu74Bn2ULx8Bn1gEbtaKi7KbeQQvD+ufyD/xb65+F6wcz3XkJ", + "TWQUf2d8f4IOCE9i9zXRgAuiJnZjFpHEid3rDT0P7zzQmxWRJHloAn0s8izcYwVS/5MiR9is8hitNN8/", + "MsUNHupSqZjtH4d+n5z9PGA9t7ZzMnehzuE6JYBBY1VpZF5GWKILGFPvQivfL+HXvv2v0/0AU/Ey4dPL", + "Q6O6o4RPUUKZzQfwApW1eGDXEj4yMDTFdxaVxhWJ2zCSxL/+8U8YFGXTf/3jnxbb/V//+Ccc9y0DrwY1", + "pi9nBAs1JlhdHqKfCMl6OKFz4iYDVWDJnIgF2h1Ymz88Ql6peyulySEbsjdE5YJ5eROmXpu0DVpXgZ4P", + "ZTmRFsZHv0gntpiMiW0M2G3cWTZL+aAnuhuAQ4QZeBPQt6KjAcCSo6bQttVEO2GTqZlzxWhaD9NcCtZb", + "z18UuVGGentmgLdkMLDEoXMHD+yk0cbFxcvNPgJty1AFFAwC3aFsxqoR/a88aT1PMhylylBglQ1vinCG", + "xzShzuTYUO3EHMEURzPKSBlfXGCNuyYO3Ug1jzk6P0U2ELILrw7Z64stMLEqEqlckK7lBMIijJbl0LjN", + "c4EegH9RBdFhPfvukE0Ihjyh0xPDBDwQ7iIfsGiYAZAHxLhSVam81h0ygyRrkYv1wUt5TBL4CPqfYkWu", + "8aKLilq3rjpKgpVWiGVXvzxkBuvVrkEPoEqQN8w+8DMzpJ6L5LU5W4JMEq0aQwS+KfsNfW9MuEA2wtmr", + "8u+6M0mWZlh60VIcvb7Q85uCJsiNPRBaen3hdmOziyRHUUKBGiLMhmwKgUAOvJezyq4WCWUzLOJexPUl", + "4IM5XTF+nZB42sRjj30iu0dJptJP4Dj9WCfXpyZczJYnoA+xAahb7bk7se+0c93ZFv9MvjtbCPIWzjtj", + "wSWG35jV/erIa+HIC6+bc+qFPGsnDoHx/iJ+TRePFPDraG95zc0Tb8kew6KHNhy0DXhFuEDnx6cIx7Eg", + "Um7+e9v79EwNlZbyn74fNSt+jNATOxYuLOiftbdUCeSpsIM3dtQIu3nV6+v699tWpfhO401X1OEpr7z7", + "vz1qnd7mGimF3pLWvt4ka4NtqYw4lBksqaUHolFCCvGlOKc+Fa2zKpsw3uLKWSkuWfZ8euIO5MPZl23X", + "OavfDQ/AFE9qDPERGWE11dqvmv2UqPldsYsObXqF+fnLIs3Bw0lBD22KDpH5U1IX49qyaS5ogE4aL9BX", + "RBl4k/vU020PgYlfEOFOtRnowsy6mJb5FBmcFpgQWGJW676n5pV2qq9p78+k+cLy3EZisUv+VURpoeyW", + "a7VKwT21JaDvT7+FHm6l3n6+sBVLYIFFBivq2LmdwLK6geWCRZtfI1c+O0WbuMZSiRVu3iQuLNkGTanQ", + "sx5Krjtifr1xLdNZvZYyNEnodGadADGdQKye8ut3wyh3HmCURZ1sgRWxIYpPMe/3XC+y9QLPiVDo9fGp", + "WX//St36A4JW16tKjnmtvF3fvfm5R1jE48J50iyT2iefWWEy9F/J5X34U/cE81mpEw+aBMZP2H8TTI5M", + "/Huf8v+180NCxwKLxf/a+QEnGWXkf+0eJVgRqTbvjVgGD3XTPbQC84SJT+svtLpowJrYFCBj1wj8xVst", + "ZX73/p9K7DeTvpXgX6zrV9m/jezvL9dK8d9uxb0qAKaPR/JwFcQWWm149BXS5gGMppYiPUibihepBLWZ", + "cang0dPLb7ZB5bSgOP/aaGn9Lw/kyuvDke7pSRcWEipKQ0ULmz74QL4AN44HF25tvw/vCDhKx3Sa81z6", + "mYkpVtGMSJu1m5AqA35qYnd5PTcK3l8wlQ4e8up4cLn6K93fk8Rf31DDvI1Db53M795qK/Pb97XMbxBN", + "bWazLbvRdSWZNhsCrR2maVsyrkC/LgeAh8YV0kXQO62olOoCAg3icMj+t9Y/flUEp79951Io88Fg5wB+", + "J2z+23cui5KdOVIhTAlqK+gd/XICXtQpBMpCkb0yYbs+DlOzG0jPlRX4t1OQSkdyew3JUeFXDamVhuQt", + "12oNye7F/apI1dIkD64jOXoLLbjFFP9zakl/cvdIRYOT+WRCI0oYFHiBxHS5FA9oNLmvnpE7JiQz64/0", + "gokqkkhrNbLgWmsk9LKm9IMHkp2WRbQeWnt05aufZmIVz2w9WKuvldJCs8L2pdHD4GFvr4dX1J4yiRmN", + "aHnpMi10ByoDmZpQaW6y0IovS1CxPnr79meXcaYlfuHqTCnuiku5uptD5heX6qOXZdUu84JrQUvkJLYZ", + "qpCHZ8s1xQTHCWUEQnSJDCWHVUvCPeqx+PxCZbjeXSuh8oGPpa1g+nhC5aOxggcR304rhaF5qeP7pfCK", + "0+JEODg1T4pfWQYUYDwh8WkL54r3bA7r1owbYLMwtuN5giOAdtSvGdQxCxtgYAb9pgALQPAkIcKgyWW5", + "cjUQh6wYHGVejXdb4+ZSNz/KmaLJZddEyAAkiESYLSyk0pBVOsNKkTRTkNoLaeswQkEyM+Ja8Uc9aMpz", + "CW9BFq7fJcLJNV7IIbPJwOZzKJQrSGSAF5Okj37kgMOA8BRT5jFeU4HwGzlklzROyMjCKFwiKpGccaEI", + "IzFK+ZzIar8Ei4QSAZM4xnrlJErxAvDMDLSjWR+eEYMZVgFr4PrfmMUUatnpnospHw4ZRjuDAUoJZtKm", + "Xks8gQvHtoFgEJUBfYsw2hu8sF/V9g0wd93yb+jTJASZ8wiPkwUimooB/EFtwgamtrakqdGrt29ChTT7", + "VZgMbdGwysZS6Uolxl2UszK5HMznOStywfV2qVwwmKd1rBEqimvQ4mmMSYT1ejJe7QeQDHkU5SJ0Qeqt", + "9oqc/jsKjt70LmCpwqnbCWjhEYlhzxlXMzjTHI7S5rcNVFUS1Z/jogkeEi4QRh5dl0YCEuXAGjcA+e+y", + "rNjHXAXey81v3dnRx9cyAnf8DfbeU7mfgIj4ZFI5gOuvJnOAV6VMLJPwn/WcHrtSrT6LiymeMi4VjRwz", + "rFd2/6oQtlYIV69skJonXFz5slWVfn/g4qqtBmbxROnTUsT8GX6Btn09PMBufnwTPxiYjbKiiebBlbQ6", + "fRWnFIQuqqSLHeYo4WyqT1Fp6H5wS7yv1W0YHDZ9mQrjPy5Qc7QSMrI/mmqvejK2liZY7SPb6mPzIt37", + "A/h3fuEK0TRLSEqgGmzPEJve7BJhCSrnU+nhDN2OV+pT5acDG11QGpd+14lDQFduwzZAel/eriBTTfh0", + "PY5f0bkDrQsA+Q2ZqexP0KXx5lyiggdrgdag5qPrGY1mAOoHeqtu32D+4Sy7LPCMNw/RKzjIPqwzdL5h", + "sPI1rUmeEIPVN0/Ty8Pleqfvz87gI4PnZyqbXh4iV+O0uD+kfssH6dOzSLBU6BcLPbhRKOOwo5cKa32z", + "mN+mhe8r8aaHLATlx8i1bZBO0KWH6nfZADnl+O3PfPpowli3uUqAmYviyKqOQJuExZ2muAmahAH9tgeD", + "EHh1S3BBM4x7xhZcGszPfFpUKKiQMs6ytuRrhwlUPE/TFTSMNjykMqlinqu/SBUTIeBjS91NxI02cGSr", + "U+ErTagWl84d7E0gv2B0kIEMDy6VZqqdboewPO0c/mr/NU/TTrdjx+NBjd9CuF8D0lhvcDmKRe+Mh8T4", + "VSy/DcZildl7IIu1m8Oq080S+Rvzwp/eW+hsdo9IhiAf1Iy4X5II6o23avBhvACLfFqIUNbUVJPamk01", + "bpY9Pbw4dyV02oRlXNhPL9yXX4D2uy5aw40Zuek+eNjG8giecnarXJrNhIs6jNC6eI4vnpA+35YsTbUN", + "hXylzdvb+VoRppbUl1mE/SA2JdVwrniKFY2gnE8041x6ZF9g/prCW9Z8W1AmGDeMnmnD4i81qV5aQ/Cl", + "FeQPrdEKYf+R7aMPn9tg+vAX7lH5xQ+eXl5w/K4TvgHyHuqNC0omKMO5JFquylOCokWkuaKp30RwNEMR", + "zlQuCJSmIyiljKZ56oM56x2bYwCeuNxOL7tonCuUYDEFvcg8dOEuEU9TwmICFrIhmxE8p1qpEyjBirBo", + "0ZMEStrOCbrm4irhOAYlP4sx+FqgJJ4gmgIBGTslCsdYYRA1LvWJH5nMnMuiyq1RrBm5KakhHjKRs28N", + "TL9u9tIN9BIRwKGmclZUQ4xwTFgUxGe++LLZ2Oe3Bl8QVZ/oI8Xm3ImXPmawjm/1dMP5MuJ4nliAMRd2", + "G9uw+RVCr2xWIqspDY6M/j2PtJmrm+MjuXiKJV51ir8M305BdF+Mf+fxHThcoDg33XmnEsj8z+qVKRiK", + "H+4E6ZJmG+/qminKvhXLfCuet/WH+/P0Dta0L4QTdhsV+6YCQ+WkvwSWa1f1Tjz3kcyI1pbkWcUekQW7", + "mKpHE5+48LjcUzF3WoZtjmbBt33upAQG7Yuzr2y7zrZtyMFd2bazzS451T1GTlkPojTDHNyacRtZtTUd", + "/Jvmg9Rm57HMR2eRpe/gwdjiacEIDWvM8CLhOP4zhOmu8OBEXAiD6QAoEU8JU9SzGvoB+mCbKyuXdV2+", + "5Puzs80mLiHUSh4h1BPmEF5SjP4sDZTzfz0nQtDYQm+i47MTGzBLJRI566PXKYWC/1eEZGVOCeT19fX8", + "HLrFcpX0CoxFt0OYEouMU6bWjqJ89X4G8/FOtdUfmE9akOqvDunWDmmw7D89dgZcBrImzARWa6YKq7XF", + "MymbcJEauQyPea5b1zxIL5PeT1OEckITIhdSkdTEBU7yBI4bFDywRW3td2aXuxAVq0+OSVjLiEiplJQz", + "OWQ2WyMjQvetP9fteyFOQYeAwgV/PTdM8ssIn9ODMRFjWDWtGuAQQbHMzmFnC2fZVowVbgjRssP7hCH9", + "APFwSC7SMU9ohBLKriTaSOiVUU/QXKJE/7G5MqBuBN997pK9dz9ZeqVP2YQHC6IZmi2I+U+VV2XZmnNM", + "Pjm29or4h8XxH9joMFtbXxRYEJz0oMiuQ6NBuaIJ/WBYnW6ESkUjk/SDi7V7f1Yw1f6QnREl9DsYksuS", + "xGAKgHa5lQkebQ3zwWA3yihAmu0SGBwwvObHKfR4fP7OJIKSlItFd8j0P6Dht0fnxrs7wdaa4A3UVgNG", + "p1uv14QYX8Ay/RvH6JkJrsQPCG74V5fg7VE+Gs+QbDiiPFulKvHsTx9EaiW4r3aFp2lXAJilYjYbU4Ej", + "EIrlLFcxv2ZhG8KcJ3mq/2H+OF0H1qVwNHsPr34x0q4Zztpu3ASfxKG0c4qJKdj4KE4Ps2BPNWZVL5yb", + "AggxlWjA4C1wpP6M1P35zff+On6B7k67oq4Y6hdzth765rNjcBgX/no8lWNuKM3NRPHV1qdrTJutT98n", + "PLqSFgzFNxtqvQ1Aw/WPJcizdRGCmAC5mciCCBmoKiK7Q1YzQBrMHYkwUkSklOFkC+ZsGgG4amfFwnNO", + "IUU6SigkqdEYUIsSwKQGADo9GzBUuQY8j6605aL8d3xnpOJoTCKeEgfhvRlS3f6GqfqBiyoe95fCF996", + "6w+gfJiCvX0NBHlzj58ESX6GbyBUOs6tQ9mNaOMVL380pqAugr0ZdnYHctjpomFnJx129A4cYzChYoX2", + "UUpZrojsoxNj34Ik2IMBkiTiLJYOSdxZ8HYHsikl1pBlQ37lAXz3kGKPpSpYyje2kxB70O8h/T0k7aAN", + "/8DZMxl34dDFiOfKmPvtubJvxUSBeWTzwX213hn5qtu34eR/s8e3wqNglzW79LbecPYslzPSbHL72VTn", + "ydUYEKpdxUw5Q3/nY9lFjFwba7iQqr/E9/TX56aDh0DP113dBjnfzv0rbH4L2PxyrcJwiSbAUl/JjjoM", + "ZiK5ybhQgKNos90NDYEmAdgNPMIJen18OmSRZkUG3E+QlAN3siDf5hY++tsFenn8potOoHoj+jEfb/bR", + "a5YsXA1t46MZMiOJGeYVYYbGhmpJHLqezdiBeu4zWFx38EjlkM3JCHhW3F65IPFuZ0ZwDBLJH52fueks", + "gPv75md9gAB613xZbHtnpfDReUOUWPSOJoqI5WbPbJ4UK1Ar7CXtQOCs4GagJ3WH0mGflX0a2cCAU+zu", + "dAJYFR+/VjK4/6qfD+MlM3EipobcOAesTwZJBjhePK1YJjlDBXMMsUD/ui5qATRlCVtetlLBgC6bIr+/", + "IJP7St5VQXf/dz1dMNMn62jKKvukibioIbLW0+uSg2cGkNg6qiKc4YiqRRfhJLF3lL0JioiUXiH+jgXB", + "VzG/Zv0he1NUL7EJvej4/F3XOWpRTOWVacH6Yvvo9ZwImY+LwSE4aMZrDGtO4iFTHEU4ifJEixtkMiER", + "5OJCURLZ4MsthtK5x7NTdhKsoOJFtedPrnBbmCZg90qyqFPcltnqLUGiBNO0Gf7bCmoQcAihBmPdKGeI", + "skliQ6oiwaVEtqkeSeiUjhMbICT76O2MIIlTMmRZghkjAuXSRMXrofcyQaTMTYK3bgBgcg1FdVEJ7ZcJ", + "rmxoQsK5kCaaQFP4+zMkFclWkNkb0/IZzPmeZFvTuO3pkYzUtTE0m0LsK0hviKEUs+CajvLEBTA+aCi6", + "GdBjS4lP5eC/FXQ6JUKfCmyYrAnHM8faLac59JWM5cYijhfFW+2KOBatelmJXsbeSmi2UYl2HXduF/UX", + "6PyKNqL32Ue3yyL+SX/Usu9qtmp4EPbRJ87yz1Ib/8JLEmxrwCop/KmZk7yRV45qJdF2PaxW68za+8x0", + "bY2f9WiwWU8ZLQtX0mebFN4vjxAGD4vy8NBlzp42bVXQriq6aUPK/3o8+y+CAu8HyP6RUU7uAGT/ReXd", + "A9L44+GfBA/qY+XRV3zProLsnx6L/r7S5w0gPcCxNaXPG65ng1dXKkrv7Tvt1CTb4p9JgrfxjreQ392y", + "f9X6W6gM3mKtc0FrgidpphYuoM36KsugM0k/kH6DI7iIW70/V/AdQjo/H3k4Om0M6PxzFnx/lJhRW7yP", + "SnR6Eqik/sQwBv0zV7lYtvSt08MimtE5aTa6V0+wXaJMkF7GM3CuxGbB7Hq4u0xh0Z9+QLZ5i7lq/wXV", + "HwEsn8QopoJEKlmYSpyaI5g+vpFIcK0JwHMuFs1RIuaI/CB4emRns+Y+tGfKGsPKOMN00Yuxwr254zYr", + "TGifEN3p4ik1w0OUoVffow1yo4SpMYEmWvNBdFIsqSmhL4EmN/0Bbw8aLJv0AxlNx21GuaJayGtbjQVF", + "uVQ8dXt/eoI2oPrYlDC9F1rUn4Akmwk+pzGJK2PszHliVnW7YUFva3fVQkVROs4pF2ZwjyLDtLmQph9o", + "VmULRUjMmDIMg1tbl6N6pkwSv+4PU+YCcOweuVF8vcKs5rfhlB1NiVAJ0y6i4txAPG9+veae8jXnJ0O5", + "O61y27nwnNXG63b5US3Tlu6j8EORO/ewZuv3X05KD5VPMpvHms7nhULaZDb/skhw8HD3w0Oby98/4RTQ", + "V8Qp356pHBrQLYYI5meI6Y7JnCQ8S6EiObzb6XZykXQOOzOlssOtLYj9nnGpDvdePNvtfPzt4/8fAAD/", + "/2DqTitN7gEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi.yaml b/openapi.yaml index b117d2035..e85fc61b3 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. + 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: