Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions cmd/api/api/auto_standby.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package api

import (
"fmt"

"github.com/kernel/hypeman/lib/autostandby"
"github.com/kernel/hypeman/lib/oapi"
"github.com/samber/lo"
)

func toDomainAutoStandbyPolicy(policy *oapi.AutoStandbyPolicy) (*autostandby.Policy, error) {
if policy == nil {
return nil, nil
}

out := &autostandby.Policy{}
if policy.Enabled != nil {
out.Enabled = *policy.Enabled
}
if policy.IdleTimeout != nil {
out.IdleTimeout = *policy.IdleTimeout
}
if policy.IgnoreSourceCidrs != nil {
out.IgnoreSourceCIDRs = append([]string(nil), (*policy.IgnoreSourceCidrs)...)
}
if policy.IgnoreDestinationPorts != nil {
out.IgnoreDestinationPorts = make([]uint16, 0, len(*policy.IgnoreDestinationPorts))
for _, port := range *policy.IgnoreDestinationPorts {
if port < 1 || port > 65535 {
return nil, fmt.Errorf("auto_standby.ignore_destination_ports must be between 1 and 65535")
}
out.IgnoreDestinationPorts = append(out.IgnoreDestinationPorts, uint16(port))
}
}

return out, nil
}

func toOAPIAutoStandbyPolicy(policy *autostandby.Policy) *oapi.AutoStandbyPolicy {
if policy == nil {
return nil
}

out := &oapi.AutoStandbyPolicy{
Enabled: lo.ToPtr(policy.Enabled),
}
if policy.IdleTimeout != "" {
out.IdleTimeout = lo.ToPtr(policy.IdleTimeout)
}
if len(policy.IgnoreSourceCIDRs) > 0 {
out.IgnoreSourceCidrs = lo.ToPtr(append([]string(nil), policy.IgnoreSourceCIDRs...))
}
if len(policy.IgnoreDestinationPorts) > 0 {
ports := make([]int, 0, len(policy.IgnoreDestinationPorts))
for _, port := range policy.IgnoreDestinationPorts {
ports = append(ports, int(port))
}
out.IgnoreDestinationPorts = &ports
}

return out
}
19 changes: 18 additions & 1 deletion cmd/api/api/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
if request.Body.Cmd != nil {
cmd = *request.Body.Cmd
}
autoStandby, err := toDomainAutoStandbyPolicy(request.Body.AutoStandby)
if err != nil {
return oapi.CreateInstance400JSONResponse{
Code: "invalid_auto_standby",
Message: err.Error(),
}, nil
}

domainReq := instances.CreateInstanceRequest{
Name: request.Body.Name,
Expand All @@ -302,6 +309,7 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
Cmd: cmd,
SkipKernelHeaders: request.Body.SkipKernelHeaders != nil && *request.Body.SkipKernelHeaders,
SkipGuestAgent: request.Body.SkipGuestAgent != nil && *request.Body.SkipGuestAgent,
AutoStandby: autoStandby,
}
if request.Body.SnapshotPolicy != nil {
snapshotPolicy, err := toInstanceSnapshotPolicy(*request.Body.SnapshotPolicy)
Expand Down Expand Up @@ -924,9 +932,17 @@ func (s *ApiService) UpdateInstance(ctx context.Context, request oapi.UpdateInst
if request.Body.Env != nil {
env = *request.Body.Env
}
autoStandby, err := toDomainAutoStandbyPolicy(request.Body.AutoStandby)
if err != nil {
return oapi.UpdateInstance400JSONResponse{
Code: "invalid_auto_standby",
Message: err.Error(),
}, nil
}

result, err := s.InstanceManager.UpdateInstance(ctx, inst.Id, instances.UpdateInstanceRequest{
Env: env,
Env: env,
AutoStandby: autoStandby,
})
if err != nil {
switch {
Expand Down Expand Up @@ -1057,6 +1073,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
oapiPolicy := toOAPISnapshotPolicy(*inst.SnapshotPolicy)
oapiInst.SnapshotPolicy = &oapiPolicy
}
oapiInst.AutoStandby = toOAPIAutoStandbyPolicy(inst.AutoStandby)

// Convert volume attachments
if len(inst.Volumes) > 0 {
Expand Down
154 changes: 154 additions & 0 deletions cmd/api/api/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/c2h5oh/datasize"
"github.com/kernel/hypeman/lib/autostandby"
"github.com/kernel/hypeman/lib/hypervisor"
"github.com/kernel/hypeman/lib/instances"
mw "github.com/kernel/hypeman/lib/middleware"
Expand Down Expand Up @@ -276,6 +277,7 @@ func (m *captureUpdateManager) UpdateInstance(ctx context.Context, id string, re
Name: "updated-instance",
Image: "docker.io/library/alpine:latest",
Env: req.Env,
AutoStandby: req.AutoStandby,
CreatedAt: now,
HypervisorType: hypervisor.TypeCloudHypervisor,
},
Expand All @@ -297,6 +299,7 @@ func (m *captureCreateManager) CreateInstance(ctx context.Context, req instances
HotplugSize: req.HotplugSize,
OverlaySize: req.OverlaySize,
Vcpus: req.Vcpus,
AutoStandby: req.AutoStandby,
CreatedAt: now,
HypervisorType: hypervisor.TypeCloudHypervisor,
},
Expand Down Expand Up @@ -477,6 +480,49 @@ func TestCreateInstance_MapsNetworkEgressEnforcementMode(t *testing.T) {
assert.Equal(t, instances.EgressEnforcementModeHTTPHTTPSOnly, mockMgr.lastReq.NetworkEgress.EnforcementMode)
}

func TestCreateInstance_MapsAutoStandbyPolicy(t *testing.T) {
t.Parallel()
svc := newTestService(t)

origMgr := svc.InstanceManager
mockMgr := &captureCreateManager{Manager: origMgr}
svc.InstanceManager = mockMgr

enabled := true
idleTimeout := "5m"
ignoreSourceCidrs := []string{"10.0.0.0/8", "192.168.0.0/16"}
ignoreDestinationPorts := []int{22, 9000}

resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "test-auto-standby",
Image: "docker.io/library/alpine:latest",
AutoStandby: &oapi.AutoStandbyPolicy{
Enabled: &enabled,
IdleTimeout: &idleTimeout,
IgnoreSourceCidrs: &ignoreSourceCidrs,
IgnoreDestinationPorts: &ignoreDestinationPorts,
},
},
})
require.NoError(t, err)

created, ok := resp.(oapi.CreateInstance201JSONResponse)
require.True(t, ok, "expected 201 response")
require.NotNil(t, mockMgr.lastReq)
require.NotNil(t, mockMgr.lastReq.AutoStandby)
assert.True(t, mockMgr.lastReq.AutoStandby.Enabled)
assert.Equal(t, "5m", mockMgr.lastReq.AutoStandby.IdleTimeout)
assert.Equal(t, []string{"10.0.0.0/8", "192.168.0.0/16"}, mockMgr.lastReq.AutoStandby.IgnoreSourceCIDRs)
assert.Equal(t, []uint16{22, 9000}, mockMgr.lastReq.AutoStandby.IgnoreDestinationPorts)

instance := oapi.Instance(created)
require.NotNil(t, instance.AutoStandby)
require.NotNil(t, instance.AutoStandby.Enabled)
assert.True(t, *instance.AutoStandby.Enabled)
assert.Equal(t, idleTimeout, *instance.AutoStandby.IdleTimeout)
}

func TestUpdateInstance_MapsEnvPatch(t *testing.T) {
t.Parallel()
svc := newTestService(t)
Expand Down Expand Up @@ -524,6 +570,114 @@ func TestUpdateInstance_MapsEnvPatch(t *testing.T) {
assert.Equal(t, "rotated-key-456", mockMgr.lastReq.Env["OUTBOUND_OPENAI_KEY"])
}

func TestUpdateInstance_MapsAutoStandbyPatch(t *testing.T) {
t.Parallel()
svc := newTestService(t)

origMgr := svc.InstanceManager
now := time.Now()
mockMgr := &captureUpdateManager{
Manager: origMgr,
result: &instances.Instance{
StoredMetadata: instances.StoredMetadata{
Id: "inst-update-auto-standby",
Name: "inst-update-auto-standby",
Image: "docker.io/library/alpine:latest",
CreatedAt: now,
HypervisorType: hypervisor.TypeCloudHypervisor,
AutoStandby: &autostandby.Policy{
Enabled: true,
IdleTimeout: "10m0s",
},
},
State: instances.StateStopped,
},
}
svc.InstanceManager = mockMgr

enabled := true
idleTimeout := "10m"
ignoreDestinationPorts := []int{22}
resolved := &instances.Instance{
StoredMetadata: instances.StoredMetadata{
Id: "inst-update-auto-standby",
Name: "inst-update-auto-standby",
Image: "docker.io/library/alpine:latest",
CreatedAt: now,
HypervisorType: hypervisor.TypeCloudHypervisor,
},
State: instances.StateStopped,
}

resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{
Id: resolved.Id,
Body: &oapi.UpdateInstanceRequest{
AutoStandby: &oapi.AutoStandbyPolicy{
Enabled: &enabled,
IdleTimeout: &idleTimeout,
IgnoreDestinationPorts: &ignoreDestinationPorts,
},
},
})
require.NoError(t, err)
updated, ok := resp.(oapi.UpdateInstance200JSONResponse)
require.True(t, ok, "expected 200 response")

require.NotNil(t, mockMgr.lastReq)
require.NotNil(t, mockMgr.lastReq.AutoStandby)
assert.Equal(t, resolved.Id, mockMgr.lastID)
assert.True(t, mockMgr.lastReq.AutoStandby.Enabled)
assert.Equal(t, "10m", mockMgr.lastReq.AutoStandby.IdleTimeout)
assert.Equal(t, []uint16{22}, mockMgr.lastReq.AutoStandby.IgnoreDestinationPorts)

instance := oapi.Instance(updated)
require.NotNil(t, instance.AutoStandby)
require.NotNil(t, instance.AutoStandby.Enabled)
assert.True(t, *instance.AutoStandby.Enabled)
}

func TestUpdateInstance_RejectsZeroAutoStandbyIgnoreDestinationPort(t *testing.T) {
t.Parallel()
svc := newTestService(t)

origMgr := svc.InstanceManager
now := time.Now()
mockMgr := &captureUpdateManager{Manager: origMgr}
svc.InstanceManager = mockMgr

resolved := &instances.Instance{
StoredMetadata: instances.StoredMetadata{
Id: "inst-update-auto-standby",
Name: "inst-update-auto-standby",
Image: "docker.io/library/alpine:latest",
CreatedAt: now,
HypervisorType: hypervisor.TypeCloudHypervisor,
},
State: instances.StateStopped,
}
enabled := true
idleTimeout := "10m"
ignoreDestinationPorts := []int{0}

resp, err := svc.UpdateInstance(mw.WithResolvedInstance(ctx(), resolved.Id, resolved), oapi.UpdateInstanceRequestObject{
Id: resolved.Id,
Body: &oapi.UpdateInstanceRequest{
AutoStandby: &oapi.AutoStandbyPolicy{
Enabled: &enabled,
IdleTimeout: &idleTimeout,
IgnoreDestinationPorts: &ignoreDestinationPorts,
},
},
})
require.NoError(t, err)

badReq, ok := resp.(oapi.UpdateInstance400JSONResponse)
require.True(t, ok, "expected 400 response")
assert.Equal(t, "invalid_auto_standby", badReq.Code)
assert.Contains(t, badReq.Message, "between 1 and 65535")
assert.Nil(t, mockMgr.lastReq)
}

func TestUpdateInstance_RequiresBody(t *testing.T) {
t.Parallel()
svc := newTestService(t)
Expand Down
6 changes: 6 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,12 @@ func run() error {
logger.Info("starting guest memory controller")
return app.GuestMemoryController.Start(gctx)
})
if app.AutoStandbyController != nil {
grp.Go(func() error {
logger.Info("starting auto-standby controller")
return app.AutoStandbyController.Run(gctx)
})
}

// Run the server
grp.Go(func() error {
Expand Down
3 changes: 3 additions & 0 deletions cmd/api/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/google/wire"
"github.com/kernel/hypeman/cmd/api/api"
"github.com/kernel/hypeman/cmd/api/config"
"github.com/kernel/hypeman/lib/autostandby"
"github.com/kernel/hypeman/lib/builds"
"github.com/kernel/hypeman/lib/devices"
"github.com/kernel/hypeman/lib/guestmemory"
Expand Down Expand Up @@ -39,6 +40,7 @@ type application struct {
BuildManager builds.Manager
ResourceManager *resources.Manager
GuestMemoryController guestmemory.Controller
AutoStandbyController *autostandby.Controller
VMMetricsManager *vm_metrics.Manager
Registry *registry.Registry
ApiService *api.ApiService
Expand All @@ -61,6 +63,7 @@ func initializeApp() (*application, func(), error) {
providers.ProvideBuildManager,
providers.ProvideResourceManager,
providers.ProvideGuestMemoryController,
providers.ProvideAutoStandbyController,
providers.ProvideVMMetricsManager,
providers.ProvideRegistry,
api.New,
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading