Skip to content

Commit 0336f8b

Browse files
committed
Add instance expiration reaper
1 parent 4860317 commit 0336f8b

19 files changed

Lines changed: 1427 additions & 396 deletions

cmd/api/api/instances.go

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,24 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
134134
resourceTags = toMapTags(request.Body.Tags)
135135
}
136136

137+
if request.Body.Ttl != nil && request.Body.ExpiresAt != nil {
138+
return oapi.CreateInstance400JSONResponse{
139+
Code: "invalid_request",
140+
Message: "ttl and expires_at are mutually exclusive",
141+
}, nil
142+
}
143+
var ttl time.Duration
144+
if request.Body.Ttl != nil {
145+
parsedTTL, err := parseTTL(*request.Body.Ttl)
146+
if err != nil {
147+
return oapi.CreateInstance400JSONResponse{
148+
Code: "invalid_ttl",
149+
Message: err.Error(),
150+
}, nil
151+
}
152+
ttl = parsedTTL
153+
}
154+
137155
// Parse network enabled (default: true)
138156
networkEnabled := true
139157
if request.Body.Network != nil && request.Body.Network.Enabled != nil {
@@ -314,6 +332,8 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
314332
NetworkBandwidthUpload: networkBandwidthUpload,
315333
Env: env,
316334
Tags: resourceTags,
335+
TTL: ttl,
336+
ExpiresAt: request.Body.ExpiresAt,
317337
NetworkEnabled: networkEnabled,
318338
NetworkEgress: networkEgress,
319339
Credentials: credentials,
@@ -363,6 +383,11 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
363383
Code: "insufficient_resources",
364384
Message: err.Error(),
365385
}, nil
386+
case errors.Is(err, instances.ErrInvalidExpiresAt):
387+
return oapi.CreateInstance400JSONResponse{
388+
Code: "invalid_expires_at",
389+
Message: err.Error(),
390+
}, nil
366391
case errors.Is(err, instances.ErrInvalidRequest):
367392
return oapi.CreateInstance400JSONResponse{
368393
Code: "invalid_request",
@@ -986,9 +1011,8 @@ func (s *ApiService) StatInstancePath(ctx context.Context, request oapi.StatInst
9861011
return response, nil
9871012
}
9881013

989-
// UpdateInstance updates mutable properties of a running instance.
990-
// Currently supports updating env vars referenced by credential policies for key rotation.
991-
// Note: Resolution is handled by ResolveResource middleware
1014+
// UpdateInstance updates mutable instance properties.
1015+
// Note: Resolution is handled by ResolveResource middleware.
9921016
func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInstanceRequestObject) (oapi.UpdateInstanceResponseObject, error) {
9931017
inst := mw.GetResolvedInstance[instances.Instance](ctx)
9941018
if inst == nil {
@@ -1031,13 +1055,32 @@ func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInst
10311055
Message: err.Error(),
10321056
}, nil
10331057
}
1058+
if request.Body.Ttl != nil && request.Body.ExpiresAt != nil {
1059+
return oapi.UpdateInstance400JSONResponse{
1060+
Code: "invalid_request",
1061+
Message: "ttl and expires_at are mutually exclusive",
1062+
}, nil
1063+
}
1064+
var ttl *time.Duration
1065+
if request.Body.Ttl != nil {
1066+
parsedTTL, err := parseTTL(*request.Body.Ttl)
1067+
if err != nil {
1068+
return oapi.UpdateInstance400JSONResponse{
1069+
Code: "invalid_ttl",
1070+
Message: err.Error(),
1071+
}, nil
1072+
}
1073+
ttl = &parsedTTL
1074+
}
10341075

10351076
result, err := s.InstanceManager.UpdateInstance(ctx, inst.Id, instances.UpdateInstanceRequest{
10361077
Env: env,
10371078
AutoStandby: autoStandby,
10381079
HealthCheck: healthCheck,
10391080
RestartPolicy: restartPolicy,
10401081
RestartPolicySet: request.Body.RestartPolicy != nil,
1082+
TTL: ttl,
1083+
ExpiresAt: request.Body.ExpiresAt,
10411084
})
10421085
if err != nil {
10431086
switch {
@@ -1051,6 +1094,16 @@ func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInst
10511094
Code: "invalid_state",
10521095
Message: err.Error(),
10531096
}, nil
1097+
case errors.Is(err, instances.ErrInstanceExpired):
1098+
return oapi.UpdateInstance409JSONResponse{
1099+
Code: "instance_expired",
1100+
Message: err.Error(),
1101+
}, nil
1102+
case errors.Is(err, instances.ErrInvalidExpiresAt):
1103+
return oapi.UpdateInstance400JSONResponse{
1104+
Code: "invalid_expires_at",
1105+
Message: err.Error(),
1106+
}, nil
10541107
case errors.Is(err, instances.ErrInvalidRequest):
10551108
return oapi.UpdateInstance400JSONResponse{
10561109
Code: "invalid_request",
@@ -1142,6 +1195,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
11421195
DiskIoBps: diskIoBpsStr,
11431196
Network: nil,
11441197
CreatedAt: inst.CreatedAt,
1198+
ExpiresAt: inst.ExpiresAt,
11451199
StartedAt: inst.StartedAt,
11461200
StoppedAt: inst.StoppedAt,
11471201
ExitCode: inst.ExitCode,
@@ -1155,7 +1209,6 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
11551209
if inst.Platform != "" {
11561210
oapiInst.Platform = lo.ToPtr(inst.Platform)
11571211
}
1158-
11591212
if inst.ExitMessage != "" {
11601213
oapiInst.ExitMessage = lo.ToPtr(inst.ExitMessage)
11611214
}
@@ -1308,6 +1361,17 @@ func toOAPISnapshotPolicy(policy instances.SnapshotPolicy) oapi.SnapshotPolicy {
13081361
return out
13091362
}
13101363

1364+
func parseTTL(value string) (time.Duration, error) {
1365+
ttl, err := time.ParseDuration(value)
1366+
if err != nil {
1367+
return 0, fmt.Errorf("ttl must be a valid duration: %w", err)
1368+
}
1369+
if ttl < 0 {
1370+
return 0, fmt.Errorf("ttl cannot be negative")
1371+
}
1372+
return ttl, nil
1373+
}
1374+
13111375
func parseOptionalDuration(value string, field string) (*time.Duration, error) {
13121376
duration, err := time.ParseDuration(value)
13131377
if err != nil {

0 commit comments

Comments
 (0)