diff --git a/.gitignore b/.gitignore index b1b22708..614d5996 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,4 @@ scripts/utm/images/ .refs/ # Build artifacts -api +/api diff --git a/cmd/api/api/snapshots.go b/cmd/api/api/snapshots.go new file mode 100644 index 00000000..19c633e6 --- /dev/null +++ b/cmd/api/api/snapshots.go @@ -0,0 +1,209 @@ +package api + +import ( + "context" + "errors" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/instances" + "github.com/kernel/hypeman/lib/logger" + mw "github.com/kernel/hypeman/lib/middleware" + "github.com/kernel/hypeman/lib/network" + "github.com/kernel/hypeman/lib/oapi" + "github.com/samber/lo" +) + +// CreateInstanceSnapshot creates a snapshot for the resolved instance. +func (s *ApiService) CreateInstanceSnapshot(ctx context.Context, request oapi.CreateInstanceSnapshotRequestObject) (oapi.CreateInstanceSnapshotResponseObject, error) { + inst := mw.GetResolvedInstance[instances.Instance](ctx) + if inst == nil { + return oapi.CreateInstanceSnapshot500JSONResponse{Code: "internal_error", Message: "resource not resolved"}, nil + } + if request.Body == nil { + return oapi.CreateInstanceSnapshot400JSONResponse{Code: "invalid_request", Message: "request body is required"}, nil + } + + var name string + if request.Body.Name != nil { + name = *request.Body.Name + } + + result, err := s.InstanceManager.CreateSnapshot(ctx, inst.Id, instances.CreateSnapshotRequest{ + Kind: instances.SnapshotKind(request.Body.Kind), + Name: name, + }) + if err != nil { + log := logger.FromContext(ctx) + switch { + case errors.Is(err, instances.ErrNotFound): + return oapi.CreateInstanceSnapshot404JSONResponse{Code: "not_found", Message: "instance not found"}, nil + case errors.Is(err, instances.ErrInvalidRequest): + return oapi.CreateInstanceSnapshot400JSONResponse{Code: "invalid_request", Message: err.Error()}, nil + case errors.Is(err, instances.ErrInvalidState), errors.Is(err, instances.ErrAlreadyExists): + return oapi.CreateInstanceSnapshot409JSONResponse{Code: "conflict", Message: err.Error()}, nil + case errors.Is(err, instances.ErrNotSupported): + return oapi.CreateInstanceSnapshot501JSONResponse{Code: "not_supported", Message: err.Error()}, nil + default: + log.ErrorContext(ctx, "failed to create snapshot", "error", err) + return oapi.CreateInstanceSnapshot500JSONResponse{Code: "internal_error", Message: "failed to create snapshot"}, nil + } + } + + return oapi.CreateInstanceSnapshot201JSONResponse(snapshotToOAPI(*result)), nil +} + +// RestoreInstanceSnapshot restores an instance from a snapshot in-place. +func (s *ApiService) RestoreInstanceSnapshot(ctx context.Context, request oapi.RestoreInstanceSnapshotRequestObject) (oapi.RestoreInstanceSnapshotResponseObject, error) { + inst := mw.GetResolvedInstance[instances.Instance](ctx) + if inst == nil { + return oapi.RestoreInstanceSnapshot500JSONResponse{Code: "internal_error", Message: "resource not resolved"}, nil + } + if request.Body == nil { + return oapi.RestoreInstanceSnapshot400JSONResponse{Code: "invalid_request", Message: "request body is required"}, nil + } + + domainReq := instances.RestoreSnapshotRequest{} + if request.Body.TargetState != nil { + domainReq.TargetState = instances.State(*request.Body.TargetState) + } + if request.Body.TargetHypervisor != nil { + domainReq.TargetHypervisor = hypervisor.Type(*request.Body.TargetHypervisor) + } + + result, err := s.InstanceManager.RestoreSnapshot(ctx, inst.Id, request.SnapshotId, domainReq) + if err != nil { + log := logger.FromContext(ctx) + switch { + case errors.Is(err, instances.ErrNotFound), errors.Is(err, instances.ErrSnapshotNotFound): + return oapi.RestoreInstanceSnapshot404JSONResponse{Code: "not_found", Message: "instance or snapshot not found"}, nil + case errors.Is(err, instances.ErrInvalidRequest): + return oapi.RestoreInstanceSnapshot400JSONResponse{Code: "invalid_request", Message: err.Error()}, nil + case errors.Is(err, instances.ErrInvalidState): + return oapi.RestoreInstanceSnapshot409JSONResponse{Code: "invalid_state", Message: err.Error()}, nil + case errors.Is(err, instances.ErrNotSupported): + return oapi.RestoreInstanceSnapshot501JSONResponse{Code: "not_supported", Message: err.Error()}, nil + default: + log.ErrorContext(ctx, "failed to restore snapshot", "error", err) + return oapi.RestoreInstanceSnapshot500JSONResponse{Code: "internal_error", Message: "failed to restore snapshot"}, nil + } + } + + return oapi.RestoreInstanceSnapshot200JSONResponse(instanceToOAPI(*result)), nil +} + +// ListSnapshots lists centrally managed snapshots with optional filters. +func (s *ApiService) ListSnapshots(ctx context.Context, request oapi.ListSnapshotsRequestObject) (oapi.ListSnapshotsResponseObject, error) { + filter := &instances.ListSnapshotsFilter{} + if request.Params.SourceInstanceId != nil { + filter.SourceInstanceID = request.Params.SourceInstanceId + } + if request.Params.Kind != nil { + kind := instances.SnapshotKind(*request.Params.Kind) + filter.Kind = &kind + } + if request.Params.Name != nil { + filter.Name = request.Params.Name + } + if filter.SourceInstanceID == nil && filter.Kind == nil && filter.Name == nil { + filter = nil + } + + snaps, err := s.InstanceManager.ListSnapshots(ctx, filter) + if err != nil { + log := logger.FromContext(ctx) + log.ErrorContext(ctx, "failed to list snapshots", "error", err) + return oapi.ListSnapshots500JSONResponse{Code: "internal_error", Message: "failed to list snapshots"}, nil + } + + resp := make([]oapi.Snapshot, len(snaps)) + for i := range snaps { + resp[i] = snapshotToOAPI(snaps[i]) + } + return oapi.ListSnapshots200JSONResponse(resp), nil +} + +// GetSnapshot returns details for a snapshot. +func (s *ApiService) GetSnapshot(ctx context.Context, request oapi.GetSnapshotRequestObject) (oapi.GetSnapshotResponseObject, error) { + snap, err := s.InstanceManager.GetSnapshot(ctx, request.SnapshotId) + if err != nil { + log := logger.FromContext(ctx) + switch { + case errors.Is(err, instances.ErrSnapshotNotFound): + return oapi.GetSnapshot404JSONResponse{Code: "not_found", Message: "snapshot not found"}, nil + default: + log.ErrorContext(ctx, "failed to get snapshot", "error", err) + return oapi.GetSnapshot500JSONResponse{Code: "internal_error", Message: "failed to get snapshot"}, nil + } + } + return oapi.GetSnapshot200JSONResponse(snapshotToOAPI(*snap)), nil +} + +// DeleteSnapshot deletes a snapshot. +func (s *ApiService) DeleteSnapshot(ctx context.Context, request oapi.DeleteSnapshotRequestObject) (oapi.DeleteSnapshotResponseObject, error) { + err := s.InstanceManager.DeleteSnapshot(ctx, request.SnapshotId) + if err != nil { + log := logger.FromContext(ctx) + switch { + case errors.Is(err, instances.ErrSnapshotNotFound): + return oapi.DeleteSnapshot404JSONResponse{Code: "not_found", Message: "snapshot not found"}, nil + default: + log.ErrorContext(ctx, "failed to delete snapshot", "error", err) + return oapi.DeleteSnapshot500JSONResponse{Code: "internal_error", Message: "failed to delete snapshot"}, nil + } + } + return oapi.DeleteSnapshot204Response{}, nil +} + +// ForkSnapshot creates a new instance from a snapshot. +func (s *ApiService) ForkSnapshot(ctx context.Context, request oapi.ForkSnapshotRequestObject) (oapi.ForkSnapshotResponseObject, error) { + if request.Body == nil { + return oapi.ForkSnapshot400JSONResponse{Code: "invalid_request", Message: "request body is required"}, nil + } + + domainReq := instances.ForkSnapshotRequest{Name: request.Body.Name} + if request.Body.TargetState != nil { + domainReq.TargetState = instances.State(*request.Body.TargetState) + } + if request.Body.TargetHypervisor != nil { + domainReq.TargetHypervisor = hypervisor.Type(*request.Body.TargetHypervisor) + } + + result, err := s.InstanceManager.ForkSnapshot(ctx, request.SnapshotId, domainReq) + if err != nil { + log := logger.FromContext(ctx) + switch { + case errors.Is(err, instances.ErrSnapshotNotFound): + return oapi.ForkSnapshot404JSONResponse{Code: "not_found", Message: "snapshot not found"}, nil + case errors.Is(err, instances.ErrInvalidRequest): + return oapi.ForkSnapshot400JSONResponse{Code: "invalid_request", Message: err.Error()}, nil + case errors.Is(err, instances.ErrInvalidState), errors.Is(err, instances.ErrAlreadyExists), errors.Is(err, network.ErrNameExists): + return oapi.ForkSnapshot409JSONResponse{Code: "conflict", Message: err.Error()}, nil + case errors.Is(err, instances.ErrNotSupported): + return oapi.ForkSnapshot501JSONResponse{Code: "not_supported", Message: err.Error()}, nil + default: + log.ErrorContext(ctx, "failed to fork snapshot", "error", err) + return oapi.ForkSnapshot500JSONResponse{Code: "internal_error", Message: "failed to fork snapshot"}, nil + } + } + + return oapi.ForkSnapshot201JSONResponse(instanceToOAPI(*result)), nil +} + +func snapshotToOAPI(snapshot instances.Snapshot) oapi.Snapshot { + kind := oapi.SnapshotKind(snapshot.Kind) + sourceHypervisor := oapi.SnapshotSourceHypervisor(snapshot.SourceHypervisor) + out := oapi.Snapshot{ + Id: snapshot.Id, + Kind: kind, + SourceInstanceId: snapshot.SourceInstanceID, + SourceInstanceName: snapshot.SourceName, + SourceHypervisor: sourceHypervisor, + CreatedAt: snapshot.CreatedAt, + SizeBytes: snapshot.SizeBytes, + Name: lo.ToPtr(snapshot.Name), + } + if snapshot.Name == "" { + out.Name = nil + } + return out +} diff --git a/lib/builds/manager_test.go b/lib/builds/manager_test.go index 27e46346..a3765d5f 100644 --- a/lib/builds/manager_test.go +++ b/lib/builds/manager_test.go @@ -49,6 +49,14 @@ func (m *mockInstanceManager) ListInstances(ctx context.Context, filter *instanc return result, nil } +func (m *mockInstanceManager) ListSnapshots(ctx context.Context, filter *instances.ListSnapshotsFilter) ([]instances.Snapshot, error) { + return nil, nil +} + +func (m *mockInstanceManager) GetSnapshot(ctx context.Context, snapshotID string) (*instances.Snapshot, error) { + return nil, instances.ErrSnapshotNotFound +} + func (m *mockInstanceManager) CreateInstance(ctx context.Context, req instances.CreateInstanceRequest) (*instances.Instance, error) { m.createCallCount++ if m.createFunc != nil { @@ -75,6 +83,10 @@ func (m *mockInstanceManager) GetInstance(ctx context.Context, id string) (*inst return nil, instances.ErrNotFound } +func (m *mockInstanceManager) CreateSnapshot(ctx context.Context, id string, req instances.CreateSnapshotRequest) (*instances.Snapshot, error) { + return nil, instances.ErrNotSupported +} + func (m *mockInstanceManager) DeleteInstance(ctx context.Context, id string) error { m.deleteCallCount++ if m.deleteFunc != nil { @@ -84,10 +96,18 @@ func (m *mockInstanceManager) DeleteInstance(ctx context.Context, id string) err return nil } +func (m *mockInstanceManager) DeleteSnapshot(ctx context.Context, snapshotID string) error { + return instances.ErrSnapshotNotFound +} + func (m *mockInstanceManager) ForkInstance(ctx context.Context, id string, req instances.ForkInstanceRequest) (*instances.Instance, error) { return nil, instances.ErrNotFound } +func (m *mockInstanceManager) ForkSnapshot(ctx context.Context, snapshotID string, req instances.ForkSnapshotRequest) (*instances.Instance, error) { + return nil, instances.ErrNotFound +} + func (m *mockInstanceManager) StandbyInstance(ctx context.Context, id string) (*instances.Instance, error) { return nil, nil } @@ -96,6 +116,10 @@ func (m *mockInstanceManager) RestoreInstance(ctx context.Context, id string) (* return nil, nil } +func (m *mockInstanceManager) RestoreSnapshot(ctx context.Context, id string, snapshotID string, req instances.RestoreSnapshotRequest) (*instances.Instance, error) { + return nil, instances.ErrNotSupported +} + func (m *mockInstanceManager) StopInstance(ctx context.Context, id string) (*instances.Instance, error) { if m.stopFunc != nil { return m.stopFunc(ctx, id) diff --git a/lib/instances/errors.go b/lib/instances/errors.go index ef30ca4c..33246fe5 100644 --- a/lib/instances/errors.go +++ b/lib/instances/errors.go @@ -26,4 +26,7 @@ var ( // ErrNotSupported is returned when an operation is not supported for the instance hypervisor ErrNotSupported = errors.New("operation not supported") + + // ErrSnapshotNotFound is returned when a snapshot is not found. + ErrSnapshotNotFound = errors.New("snapshot not found") ) diff --git a/lib/instances/firecracker_test.go b/lib/instances/firecracker_test.go index 9579ea68..24647f05 100644 --- a/lib/instances/firecracker_test.go +++ b/lib/instances/firecracker_test.go @@ -381,3 +381,15 @@ func TestFirecrackerForkFromRunningNetwork(t *testing.T) { assert.NotEqual(t, sourceAfterFork.IP, forked.IP) assert.NotEqual(t, sourceAfterFork.MAC, forked.MAC) } + +func TestFirecrackerSnapshotFeature(t *testing.T) { + requireFirecrackerIntegrationPrereqs(t) + + mgr, tmpDir := setupTestManagerForFirecracker(t) + runStandbySnapshotScenario(t, mgr, tmpDir, snapshotScenarioConfig{ + hypervisor: hypervisor.TypeFirecracker, + sourceName: "fc-snapshot-src", + snapshot: "fc-snapshot-1", + forkName: "fc-snapshot-fork", + }) +} diff --git a/lib/instances/fork.go b/lib/instances/fork.go index 028e6b3b..60d3a5cf 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -392,41 +392,53 @@ func (m *manager) applyForkTargetState(ctx context.Context, forkID string, targe lock.Lock() defer lock.Unlock() + returnWithReadiness := func(inst *Instance, err error) (*Instance, error) { + if err != nil { + return nil, err + } + if inst != nil && inst.State == StateRunning { + if err := ensureGuestAgentReadyForForkPhase(ctx, &inst.StoredMetadata, "before returning running fork instance"); err != nil { + return nil, fmt.Errorf("wait for forked guest agent readiness: %w", err) + } + } + return inst, nil + } + current, err := m.getInstance(ctx, forkID) if err != nil { return nil, err } if current.State == target { - return current, nil + return returnWithReadiness(current, nil) } switch current.State { case StateStopped: switch target { case StateRunning: - return m.startInstance(ctx, forkID, StartInstanceRequest{}) + return returnWithReadiness(m.startInstance(ctx, forkID, StartInstanceRequest{})) case StateStandby: if _, err := m.startInstance(ctx, forkID, StartInstanceRequest{}); err != nil { return nil, fmt.Errorf("start forked instance for standby transition: %w", err) } - return m.standbyInstance(ctx, forkID) + return returnWithReadiness(m.standbyInstance(ctx, forkID)) } case StateStandby: switch target { case StateRunning: - return m.restoreInstance(ctx, forkID) + return returnWithReadiness(m.restoreInstance(ctx, forkID)) case StateStopped: if err := os.RemoveAll(m.paths.InstanceSnapshotLatest(forkID)); err != nil { return nil, fmt.Errorf("remove fork snapshot: %w", err) } - return m.getInstance(ctx, forkID) + return returnWithReadiness(m.getInstance(ctx, forkID)) } case StateRunning: switch target { case StateStandby: - return m.standbyInstance(ctx, forkID) + return returnWithReadiness(m.standbyInstance(ctx, forkID)) case StateStopped: - return m.stopInstance(ctx, forkID) + return returnWithReadiness(m.stopInstance(ctx, forkID)) } } diff --git a/lib/instances/manager.go b/lib/instances/manager.go index d06ea48e..3b581e83 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -19,15 +19,21 @@ import ( type Manager interface { ListInstances(ctx context.Context, filter *ListInstancesFilter) ([]Instance, error) + ListSnapshots(ctx context.Context, filter *ListSnapshotsFilter) ([]Snapshot, error) + GetSnapshot(ctx context.Context, snapshotID string) (*Snapshot, error) CreateInstance(ctx context.Context, req CreateInstanceRequest) (*Instance, error) + CreateSnapshot(ctx context.Context, id string, req CreateSnapshotRequest) (*Snapshot, error) // GetInstance returns an instance by ID, name, or ID prefix. // Lookup order: exact ID match -> exact name match -> ID prefix match. // Returns ErrAmbiguousName if prefix matches multiple instances. GetInstance(ctx context.Context, idOrName string) (*Instance, error) DeleteInstance(ctx context.Context, id string) error + DeleteSnapshot(ctx context.Context, snapshotID string) error ForkInstance(ctx context.Context, id string, req ForkInstanceRequest) (*Instance, error) + ForkSnapshot(ctx context.Context, snapshotID string, req ForkSnapshotRequest) (*Instance, error) StandbyInstance(ctx context.Context, id string) (*Instance, error) RestoreInstance(ctx context.Context, id string) (*Instance, error) + RestoreSnapshot(ctx context.Context, id string, snapshotID string, req RestoreSnapshotRequest) (*Instance, error) StopInstance(ctx context.Context, id string) (*Instance, error) StartInstance(ctx context.Context, id string, req StartInstanceRequest) (*Instance, error) StreamInstanceLogs(ctx context.Context, id string, tail int, follow bool, source LogSource) (<-chan string, error) @@ -183,6 +189,25 @@ func (m *manager) DeleteInstance(ctx context.Context, id string) error { return err } +func (m *manager) ListSnapshots(ctx context.Context, filter *ListSnapshotsFilter) ([]Snapshot, error) { + return m.listSnapshots(ctx, filter) +} + +func (m *manager) GetSnapshot(ctx context.Context, snapshotID string) (*Snapshot, error) { + return m.getSnapshot(ctx, snapshotID) +} + +func (m *manager) CreateSnapshot(ctx context.Context, id string, req CreateSnapshotRequest) (*Snapshot, error) { + lock := m.getInstanceLock(id) + lock.Lock() + defer lock.Unlock() + return m.createSnapshot(ctx, id, req) +} + +func (m *manager) DeleteSnapshot(ctx context.Context, snapshotID string) error { + return m.deleteSnapshot(ctx, snapshotID) +} + // ForkInstance creates a forked copy of an instance. func (m *manager) ForkInstance(ctx context.Context, id string, req ForkInstanceRequest) (*Instance, error) { lock := m.getInstanceLock(id) @@ -211,6 +236,10 @@ func (m *manager) ForkInstance(ctx context.Context, id string, req ForkInstanceR return inst, nil } +func (m *manager) ForkSnapshot(ctx context.Context, snapshotID string, req ForkSnapshotRequest) (*Instance, error) { + return m.forkSnapshot(ctx, snapshotID, req) +} + // StandbyInstance puts an instance in standby (pause, snapshot, delete VMM) func (m *manager) StandbyInstance(ctx context.Context, id string) (*Instance, error) { lock := m.getInstanceLock(id) @@ -227,6 +256,13 @@ func (m *manager) RestoreInstance(ctx context.Context, id string) (*Instance, er return m.restoreInstance(ctx, id) } +func (m *manager) RestoreSnapshot(ctx context.Context, id string, snapshotID string, req RestoreSnapshotRequest) (*Instance, error) { + lock := m.getInstanceLock(id) + lock.Lock() + defer lock.Unlock() + return m.restoreSnapshot(ctx, id, snapshotID, req) +} + // StopInstance gracefully stops a running instance func (m *manager) StopInstance(ctx context.Context, id string) (*Instance, error) { lock := m.getInstanceLock(id) diff --git a/lib/instances/manager_darwin_test.go b/lib/instances/manager_darwin_test.go index 719f89c4..e198aa34 100644 --- a/lib/instances/manager_darwin_test.go +++ b/lib/instances/manager_darwin_test.go @@ -732,3 +732,27 @@ func ensureMkfsExt4Available(t *testing.T) { t.Fatalf("mkfs.ext4 not found; install e2fsprogs and ensure it is on PATH") } + +func TestVZSnapshotFeature(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("vz tests require macOS") + } + if runtime.GOARCH != "arm64" { + t.Skip("vz tests require Apple Silicon (arm64)") + } + if !isMacOS14OrLater(t) { + t.Skip("vz snapshot test requires macOS 14+") + } + ensureMkfsExt4Available(t) + + mgr, tmpDir := setupVZTestManager(t) + runStandbySnapshotScenario(t, mgr, tmpDir, snapshotScenarioConfig{ + hypervisor: hypervisor.TypeVZ, + sourceName: "vz-snapshot-src", + snapshot: "vz-snapshot-1", + forkName: "vz-snapshot-fork", + onError: func() { + dumpVZShimLogs(t, tmpDir) + }, + }) +} diff --git a/lib/instances/manager_test.go b/lib/instances/manager_test.go index 18b29711..3ff4d790 100644 --- a/lib/instances/manager_test.go +++ b/lib/instances/manager_test.go @@ -1357,6 +1357,20 @@ func TestStandbyAndRestore(t *testing.T) { t.Log("Standby/restore test complete!") } +func TestCloudHypervisorSnapshotFeature(t *testing.T) { + if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) { + t.Skip("/dev/kvm not available, skipping on this platform") + } + + mgr, tmpDir := setupTestManager(t) + runStandbySnapshotScenario(t, mgr, tmpDir, snapshotScenarioConfig{ + hypervisor: hypervisor.TypeCloudHypervisor, + sourceName: "ch-snapshot-src", + snapshot: "ch-snapshot-1", + forkName: "ch-snapshot-fork", + }) +} + func TestStateTransitions(t *testing.T) { tests := []struct { name string diff --git a/lib/instances/qemu_test.go b/lib/instances/qemu_test.go index 78d5a12f..650aaa7f 100644 --- a/lib/instances/qemu_test.go +++ b/lib/instances/qemu_test.go @@ -954,3 +954,22 @@ func TestQEMUForkFromRunningNetwork(t *testing.T) { assertHostCanReachNginx(t, forked.IP, 80, 60*time.Second) assertHostCanReachNginx(t, sourceAfterFork.IP, 80, 60*time.Second) } + +func TestQEMUSnapshotFeature(t *testing.T) { + if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) { + t.Skip("/dev/kvm not available, skipping on this platform") + } + + starter := qemu.NewStarter() + if _, err := starter.GetBinaryPath(nil, ""); err != nil { + t.Skipf("QEMU not available: %v", err) + } + + mgr, tmpDir := setupTestManagerForQEMU(t) + runStandbySnapshotScenario(t, mgr, tmpDir, snapshotScenarioConfig{ + hypervisor: hypervisor.TypeQEMU, + sourceName: "qemu-snapshot-src", + snapshot: "qemu-snapshot-1", + forkName: "qemu-snapshot-fork", + }) +} diff --git a/lib/instances/snapshot.go b/lib/instances/snapshot.go new file mode 100644 index 00000000..0ea65d4e --- /dev/null +++ b/lib/instances/snapshot.go @@ -0,0 +1,550 @@ +package instances + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "github.com/kernel/hypeman/lib/forkvm" + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/logger" + "github.com/kernel/hypeman/lib/network" + snapshotstore "github.com/kernel/hypeman/lib/snapshot" + "github.com/nrednav/cuid2" + "gvisor.dev/gvisor/pkg/cleanup" +) + +type snapshotRecord struct { + Snapshot Snapshot + StoredMetadata StoredMetadata +} + +func (m *manager) listSnapshots(ctx context.Context, filter *ListSnapshotsFilter) ([]Snapshot, error) { + _ = ctx + snapshots, err := m.snapshotStore().List(filter) + if err != nil { + return nil, fmt.Errorf("list snapshots: %w", err) + } + return snapshots, nil +} + +func (m *manager) getSnapshot(ctx context.Context, snapshotID string) (*Snapshot, error) { + _ = ctx + snapshot, err := m.snapshotStore().Get(snapshotID) + if err != nil { + if errors.Is(err, snapshotstore.ErrNotFound) { + return nil, ErrSnapshotNotFound + } + return nil, err + } + return snapshot, nil +} + +func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnapshotRequest) (*Snapshot, error) { + log := logger.FromContext(ctx) + log.InfoContext(ctx, "creating snapshot", "instance_id", id, "kind", req.Kind, "name", req.Name) + + if err := validateCreateSnapshotRequest(req); err != nil { + return nil, err + } + + meta, err := m.loadMetadata(id) + if err != nil { + return nil, err + } + inst := m.toInstance(ctx, meta) + stored := &meta.StoredMetadata + + if err := validateForkVolumeSafety(stored.Volumes); err != nil { + return nil, fmt.Errorf("%w: snapshot requires readonly volume attachments: %v", ErrNotSupported, err) + } + if err := m.ensureSnapshotNameAvailable(stored.Id, req.Name); err != nil { + return nil, err + } + + snapshotID := cuid2.Generate() + if _, err := m.loadSnapshotRecord(snapshotID); err == nil { + return nil, fmt.Errorf("%w: generated snapshot id already exists", ErrAlreadyExists) + } else if !errors.Is(err, ErrSnapshotNotFound) { + return nil, err + } + + snapshotDir := m.paths.SnapshotDir(snapshotID) + snapshotGuestDir := m.paths.SnapshotGuestDir(snapshotID) + cu := cleanup.Make(func() { + _ = os.RemoveAll(snapshotDir) + }) + defer cu.Clean() + + if err := os.MkdirAll(m.paths.SnapshotStoreDir(), 0755); err != nil { + return nil, fmt.Errorf("create snapshot store dir: %w", err) + } + + switch req.Kind { + case SnapshotKindStandby: + restoreSource := false + switch inst.State { + case StateRunning: + if err := ensureGuestAgentReadyForForkPhase(ctx, &inst.StoredMetadata, "before running snapshot"); err != nil { + return nil, err + } + if _, err := m.standbyInstance(ctx, id); err != nil { + return nil, fmt.Errorf("standby source instance: %w", err) + } + restoreSource = true + case StateStandby: + // already ready to copy + default: + return nil, fmt.Errorf("%w: standby snapshot requires source in %s or %s, got %s", ErrInvalidState, StateRunning, StateStandby, inst.State) + } + + copyErr := m.copySnapshotPayload(id, snapshotGuestDir) + if copyErr == nil { + meta, copyErr = m.loadMetadata(id) + } + + if restoreSource { + _, restoreErr := m.restoreInstance(ctx, id) + if restoreErr != nil { + if copyErr != nil { + return nil, fmt.Errorf("snapshot copy failed: %v; additionally failed to restore source: %w", copyErr, restoreErr) + } + return nil, fmt.Errorf("restore source after snapshot: %w", restoreErr) + } + } + + if copyErr != nil { + return nil, copyErr + } + + rec := &snapshotRecord{ + Snapshot: Snapshot{ + Id: snapshotID, + Name: req.Name, + Kind: req.Kind, + SourceInstanceID: stored.Id, + SourceName: stored.Name, + SourceHypervisor: stored.HypervisorType, + CreatedAt: time.Now(), + }, + StoredMetadata: cloneStoredMetadataForFork(meta.StoredMetadata), + } + sizeBytes, err := snapshotstore.DirectoryFileSize(snapshotGuestDir) + if err != nil { + return nil, err + } + rec.Snapshot.SizeBytes = sizeBytes + if err := m.saveSnapshotRecord(rec); err != nil { + return nil, err + } + cu.Release() + log.InfoContext(ctx, "snapshot created", "instance_id", id, "snapshot_id", snapshotID, "kind", req.Kind) + return &rec.Snapshot, nil + + case SnapshotKindStopped: + if inst.State != StateStopped { + return nil, fmt.Errorf("%w: stopped snapshot requires source in %s, got %s", ErrInvalidState, StateStopped, inst.State) + } + if err := m.copySnapshotPayload(id, snapshotGuestDir); err != nil { + return nil, err + } + rec := &snapshotRecord{ + Snapshot: Snapshot{ + Id: snapshotID, + Name: req.Name, + Kind: req.Kind, + SourceInstanceID: stored.Id, + SourceName: stored.Name, + SourceHypervisor: stored.HypervisorType, + CreatedAt: time.Now(), + }, + StoredMetadata: cloneStoredMetadataForFork(meta.StoredMetadata), + } + sizeBytes, err := snapshotstore.DirectoryFileSize(snapshotGuestDir) + if err != nil { + return nil, err + } + rec.Snapshot.SizeBytes = sizeBytes + if err := m.saveSnapshotRecord(rec); err != nil { + return nil, err + } + cu.Release() + log.InfoContext(ctx, "snapshot created", "instance_id", id, "snapshot_id", snapshotID, "kind", req.Kind) + return &rec.Snapshot, nil + + default: + return nil, fmt.Errorf("%w: unsupported snapshot kind %q", ErrInvalidRequest, req.Kind) + } +} + +func (m *manager) deleteSnapshot(ctx context.Context, snapshotID string) error { + _ = ctx + if err := m.snapshotStore().Delete(snapshotID); err != nil { + if errors.Is(err, snapshotstore.ErrNotFound) { + return ErrSnapshotNotFound + } + return err + } + return nil +} + +func (m *manager) restoreSnapshot(ctx context.Context, id string, snapshotID string, req RestoreSnapshotRequest) (*Instance, error) { + log := logger.FromContext(ctx) + rec, err := m.loadSnapshotRecord(snapshotID) + if err != nil { + return nil, err + } + if rec.Snapshot.SourceInstanceID != id { + return nil, fmt.Errorf("%w: snapshot %s belongs to instance %s", ErrInvalidRequest, snapshotID, rec.Snapshot.SourceInstanceID) + } + + sourceMeta, err := m.loadMetadata(id) + if err != nil { + return nil, err + } + sourceInst := m.toInstance(ctx, sourceMeta) + if sourceInst.State == StateRunning { + return nil, fmt.Errorf("%w: cannot restore snapshot while source is %s", ErrInvalidState, sourceInst.State) + } + + targetState, err := resolveSnapshotTargetState(rec.Snapshot.Kind, req.TargetState) + if err != nil { + return nil, err + } + targetHypervisor, err := m.resolveSnapshotTargetHypervisor(rec, req.TargetHypervisor) + if err != nil { + return nil, err + } + + if err := m.replaceInstanceWithSnapshotPayload(snapshotID, id); err != nil { + return nil, err + } + + restored := cloneStoredMetadataForFork(rec.StoredMetadata) + restored.Id = sourceMeta.Id + restored.Name = sourceMeta.Name + restored.DataDir = m.paths.InstanceDir(id) + restored.HypervisorPID = nil + restored.StartedAt = nil + restored.StoppedAt = nil + restored.ExitCode = nil + restored.ExitMessage = "" + restored.HypervisorType = targetHypervisor + + starter, err := m.getVMStarter(targetHypervisor) + if err != nil { + return nil, fmt.Errorf("get vm starter: %w", err) + } + hvVersion, err := starter.GetVersion(m.paths) + if err != nil { + log.WarnContext(ctx, "failed to get hypervisor version", "hypervisor", targetHypervisor, "error", err) + hvVersion = "unknown" + } + restored.HypervisorVersion = hvVersion + restored.SocketPath = m.paths.InstanceSocket(id, starter.SocketName()) + restored.VsockSocket = m.paths.InstanceSocket(id, hypervisor.VsockSocketNameForType(targetHypervisor)) + if rec.Snapshot.Kind == SnapshotKindStopped { + restored.VsockCID = generateVsockCID(id) + } + + if err := m.saveMetadata(&metadata{StoredMetadata: restored}); err != nil { + return nil, fmt.Errorf("save restored metadata: %w", err) + } + + switch rec.Snapshot.Kind { + case SnapshotKindStandby: + switch targetState { + case StateStandby: + return m.getInstance(ctx, id) + case StateStopped: + if err := os.RemoveAll(m.paths.InstanceSnapshotLatest(id)); err != nil { + return nil, fmt.Errorf("remove instance snapshot: %w", err) + } + return m.getInstance(ctx, id) + case StateRunning: + inst, err := m.restoreInstance(ctx, id) + if err != nil { + return nil, err + } + if err := ensureGuestAgentReadyForForkPhase(ctx, &inst.StoredMetadata, "before returning running snapshot restore instance"); err != nil { + return nil, fmt.Errorf("wait for snapshot restore guest agent readiness: %w", err) + } + return inst, nil + } + case SnapshotKindStopped: + switch targetState { + case StateStopped: + _ = os.RemoveAll(m.paths.InstanceSnapshotLatest(id)) + return m.getInstance(ctx, id) + case StateRunning: + inst, err := m.startInstance(ctx, id, StartInstanceRequest{}) + if err != nil { + return nil, err + } + if err := ensureGuestAgentReadyForForkPhase(ctx, &inst.StoredMetadata, "before returning running snapshot restore instance"); err != nil { + return nil, fmt.Errorf("wait for snapshot restore guest agent readiness: %w", err) + } + return inst, nil + } + } + + return nil, fmt.Errorf("%w: unsupported restore target state %s for snapshot kind %s", ErrInvalidRequest, targetState, rec.Snapshot.Kind) +} + +func (m *manager) forkSnapshot(ctx context.Context, snapshotID string, req ForkSnapshotRequest) (*Instance, error) { + if err := validateForkSnapshotRequest(req); err != nil { + return nil, err + } + + rec, err := m.loadSnapshotRecord(snapshotID) + if err != nil { + return nil, err + } + if err := validateForkVolumeSafety(rec.StoredMetadata.Volumes); err != nil { + return nil, fmt.Errorf("%w: snapshot requires readonly volume attachments: %v", ErrNotSupported, err) + } + + if err := m.ensureInstanceNameAvailableForSnapshotFork(ctx, req.Name, rec.StoredMetadata.NetworkEnabled); err != nil { + return nil, err + } + + targetState, err := resolveSnapshotTargetState(rec.Snapshot.Kind, req.TargetState) + if err != nil { + return nil, err + } + targetHypervisor, err := m.resolveSnapshotTargetHypervisor(rec, req.TargetHypervisor) + if err != nil { + return nil, err + } + + forkID := cuid2.Generate() + if _, err := m.loadMetadata(forkID); err == nil { + return nil, fmt.Errorf("%w: generated fork id already exists", ErrAlreadyExists) + } else if !errors.Is(err, ErrNotFound) { + return nil, err + } + + dstDir := m.paths.InstanceDir(forkID) + cu := cleanup.Make(func() { + _ = os.RemoveAll(dstDir) + }) + defer cu.Clean() + + if err := forkvm.CopyGuestDirectory(m.paths.SnapshotGuestDir(snapshotID), dstDir); err != nil { + if errors.Is(err, forkvm.ErrSparseCopyUnsupported) { + return nil, fmt.Errorf("fork from snapshot requires sparse-capable filesystem (SEEK_DATA/SEEK_HOLE unsupported): %w", err) + } + return nil, fmt.Errorf("clone snapshot payload: %w", err) + } + + starter, err := m.getVMStarter(targetHypervisor) + if err != nil { + return nil, fmt.Errorf("get vm starter: %w", err) + } + hvVersion, err := starter.GetVersion(m.paths) + if err != nil { + hvVersion = "unknown" + } + + now := time.Now() + forkMeta := cloneStoredMetadataForFork(rec.StoredMetadata) + forkMeta.Id = forkID + forkMeta.Name = req.Name + forkMeta.CreatedAt = now + forkMeta.StartedAt = nil + forkMeta.StoppedAt = nil + forkMeta.HypervisorPID = nil + forkMeta.DataDir = dstDir + forkMeta.HypervisorType = targetHypervisor + forkMeta.HypervisorVersion = hvVersion + forkMeta.SocketPath = m.paths.InstanceSocket(forkID, starter.SocketName()) + forkMeta.VsockSocket = m.paths.InstanceSocket(forkID, hypervisor.VsockSocketNameForType(targetHypervisor)) + forkMeta.ExitCode = nil + forkMeta.ExitMessage = "" + if rec.Snapshot.Kind == SnapshotKindStandby { + forkMeta.VsockCID = rec.StoredMetadata.VsockCID + } else { + forkMeta.VsockCID = generateVsockCID(forkID) + } + if forkMeta.NetworkEnabled { + forkMeta.IP = "" + forkMeta.MAC = "" + } + + if rec.Snapshot.Kind == SnapshotKindStandby { + netCfg := (*hypervisor.ForkNetworkConfig)(nil) + if forkMeta.NetworkEnabled { + netCfg = &hypervisor.ForkNetworkConfig{TAPDevice: network.GenerateTAPName(forkID)} + } + if _, err := starter.PrepareFork(ctx, hypervisor.ForkPrepareRequest{ + SnapshotConfigPath: m.paths.InstanceSnapshotConfig(forkID), + SourceDataDir: rec.StoredMetadata.DataDir, + TargetDataDir: forkMeta.DataDir, + VsockCID: forkMeta.VsockCID, + VsockSocket: forkMeta.VsockSocket, + SerialLogPath: m.paths.InstanceAppLog(forkID), + Network: netCfg, + }); err != nil { + if errors.Is(err, hypervisor.ErrNotSupported) { + return nil, fmt.Errorf("%w: snapshot fork is not supported for hypervisor %s", ErrNotSupported, targetHypervisor) + } + return nil, fmt.Errorf("prepare snapshot fork state: %w", err) + } + } + + if err := m.saveMetadata(&metadata{StoredMetadata: forkMeta}); err != nil { + return nil, fmt.Errorf("save fork metadata: %w", err) + } + + cu.Release() + inst, err := m.applyForkTargetState(ctx, forkID, targetState) + if err != nil { + if cleanupErr := m.cleanupForkInstanceOnError(ctx, forkID); cleanupErr != nil { + return nil, fmt.Errorf("apply snapshot fork target state: %w; additionally failed to cleanup forked instance %s: %v", err, forkID, cleanupErr) + } + return nil, fmt.Errorf("apply snapshot fork target state: %w", err) + } + return inst, nil +} + +func (m *manager) copySnapshotPayload(sourceInstanceID, snapshotGuestDir string) error { + if err := forkvm.CopyGuestDirectory(m.paths.InstanceDir(sourceInstanceID), snapshotGuestDir); err != nil { + if errors.Is(err, forkvm.ErrSparseCopyUnsupported) { + return fmt.Errorf("snapshot requires sparse-capable filesystem (SEEK_DATA/SEEK_HOLE unsupported): %w", err) + } + return fmt.Errorf("copy guest directory into snapshot: %w", err) + } + return nil +} + +func (m *manager) replaceInstanceWithSnapshotPayload(snapshotID, instanceID string) error { + instanceDir := m.paths.InstanceDir(instanceID) + if err := os.RemoveAll(instanceDir); err != nil { + return fmt.Errorf("clear instance directory: %w", err) + } + if err := forkvm.CopyGuestDirectory(m.paths.SnapshotGuestDir(snapshotID), instanceDir); err != nil { + if errors.Is(err, forkvm.ErrSparseCopyUnsupported) { + return fmt.Errorf("restore requires sparse-capable filesystem (SEEK_DATA/SEEK_HOLE unsupported): %w", err) + } + return fmt.Errorf("restore snapshot payload: %w", err) + } + return nil +} + +func (m *manager) resolveSnapshotTargetHypervisor(rec *snapshotRecord, requested hypervisor.Type) (hypervisor.Type, error) { + if requested == "" { + return rec.StoredMetadata.HypervisorType, nil + } + if rec.Snapshot.Kind == SnapshotKindStandby { + return "", fmt.Errorf("%w: target_hypervisor is only allowed for stopped snapshots", ErrInvalidRequest) + } + if _, err := m.getVMStarter(requested); err != nil { + return "", fmt.Errorf("%w: unsupported target hypervisor %q", ErrInvalidRequest, requested) + } + return requested, nil +} + +func resolveSnapshotTargetState(kind SnapshotKind, requested State) (State, error) { + resolved, err := snapshotstore.ResolveTargetState(kind, string(requested)) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrInvalidRequest, err) + } + return State(resolved), nil +} + +func validateCreateSnapshotRequest(req CreateSnapshotRequest) error { + if req.Kind != SnapshotKindStandby && req.Kind != SnapshotKindStopped { + return fmt.Errorf("%w: kind must be one of %s, %s", ErrInvalidRequest, SnapshotKindStandby, SnapshotKindStopped) + } + if req.Name != "" { + if err := validateInstanceName(req.Name); err != nil { + return fmt.Errorf("%w: %v", ErrInvalidRequest, err) + } + } + return nil +} + +func validateForkSnapshotRequest(req ForkSnapshotRequest) error { + if err := validateInstanceName(req.Name); err != nil { + return fmt.Errorf("%w: %v", ErrInvalidRequest, err) + } + if req.TargetState != "" && req.TargetState != StateStopped && req.TargetState != StateStandby && req.TargetState != StateRunning { + return fmt.Errorf("%w: invalid target_state %q", ErrInvalidRequest, req.TargetState) + } + return nil +} + +func (m *manager) snapshotStore() *snapshotstore.Store { + return snapshotstore.NewStore(m.paths) +} + +func (m *manager) ensureSnapshotNameAvailable(sourceInstanceID, snapshotName string) error { + if err := m.snapshotStore().EnsureNameAvailable(sourceInstanceID, snapshotName); err != nil { + if errors.Is(err, snapshotstore.ErrNameExists) { + return fmt.Errorf("%w: %v", ErrAlreadyExists, err) + } + return err + } + return nil +} + +func (m *manager) ensureInstanceNameAvailableForSnapshotFork(ctx context.Context, name string, networkEnabled bool) error { + existsByMetadata, err := m.instanceNameExists(name) + if err != nil { + return fmt.Errorf("check instance name availability: %w", err) + } + if existsByMetadata { + return fmt.Errorf("%w: instance name '%s' already exists", ErrAlreadyExists, name) + } + if networkEnabled { + exists, err := m.networkManager.NameExists(ctx, name, "") + if err != nil { + return fmt.Errorf("check instance name availability: %w", err) + } + if exists { + return fmt.Errorf("%w: instance name '%s' already exists in network", ErrAlreadyExists, name) + } + } + return nil +} + +func (m *manager) saveSnapshotRecord(rec *snapshotRecord) error { + if err := snapshotstore.SaveTypedRecord(m.snapshotStore(), &snapshotstore.TypedRecord[StoredMetadata]{ + Snapshot: rec.Snapshot, + StoredMetadata: rec.StoredMetadata, + }); err != nil { + return err + } + return nil +} + +func (m *manager) loadSnapshotRecord(snapshotID string) (*snapshotRecord, error) { + record, err := snapshotstore.LoadTypedRecord[StoredMetadata](m.snapshotStore(), snapshotID) + if err != nil { + if errors.Is(err, snapshotstore.ErrNotFound) { + return nil, ErrSnapshotNotFound + } + return nil, err + } + return &snapshotRecord{ + Snapshot: record.Snapshot, + StoredMetadata: record.StoredMetadata, + }, nil +} + +func (m *manager) listSnapshotRecords() ([]snapshotRecord, error) { + storedRecords, err := snapshotstore.ListTypedRecords[StoredMetadata](m.snapshotStore()) + if err != nil { + return nil, err + } + records := make([]snapshotRecord, 0, len(storedRecords)) + for _, stored := range storedRecords { + records = append(records, snapshotRecord{ + Snapshot: stored.Snapshot, + StoredMetadata: stored.StoredMetadata, + }) + } + return records, nil +} diff --git a/lib/instances/snapshot_integration_scenario_test.go b/lib/instances/snapshot_integration_scenario_test.go new file mode 100644 index 00000000..856f925e --- /dev/null +++ b/lib/instances/snapshot_integration_scenario_test.go @@ -0,0 +1,105 @@ +package instances + +import ( + "context" + "testing" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/paths" + snapshottest "github.com/kernel/hypeman/lib/snapshot/testsupport" + "github.com/kernel/hypeman/lib/system" + "github.com/stretchr/testify/require" +) + +type snapshotScenarioConfig struct { + hypervisor hypervisor.Type + sourceName string + snapshot string + forkName string + onError func() +} + +func runStandbySnapshotScenario(t *testing.T, mgr *manager, tmpDir string, cfg snapshotScenarioConfig) { + t.Helper() + + ctx := context.Background() + p := paths.New(tmpDir) + + onErr := func() {} + if cfg.onError != nil { + onErr = cfg.onError + } + requireNoErr := func(err error) { + t.Helper() + if err != nil { + onErr() + } + require.NoError(t, err) + } + imageManager, err := images.NewManager(p, 1, nil) + requireNoErr(err) + snapshottest.EnsureImageReady(t, ctx, p, imageManager, "docker.io/library/alpine:latest") + + systemManager := system.NewManager(p) + requireNoErr(systemManager.EnsureSystemFiles(ctx)) + + source, err := mgr.CreateInstance(ctx, CreateInstanceRequest{ + Name: cfg.sourceName, + Image: "docker.io/library/alpine:latest", + Size: 1024 * 1024 * 1024, + OverlaySize: 10 * 1024 * 1024 * 1024, + Vcpus: 1, + NetworkEnabled: false, + Hypervisor: cfg.hypervisor, + Cmd: []string{"sleep", "infinity"}, + }) + requireNoErr(err) + + sourceID := source.Id + sourceDeleted := false + t.Cleanup(func() { + if !sourceDeleted { + _ = mgr.DeleteInstance(context.Background(), sourceID) + } + }) + + _, err = mgr.StandbyInstance(ctx, sourceID) + requireNoErr(err) + + snapshot, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStandby, + Name: cfg.snapshot, + }) + requireNoErr(err) + require.Equal(t, SnapshotKindStandby, snapshot.Kind) + require.Equal(t, sourceID, snapshot.SourceInstanceID) + + filter := &ListSnapshotsFilter{SourceInstanceID: &sourceID} + snapshots, err := mgr.ListSnapshots(ctx, filter) + requireNoErr(err) + require.NotEmpty(t, snapshots) + + gotSnapshot, err := mgr.GetSnapshot(ctx, snapshot.Id) + requireNoErr(err) + require.Equal(t, snapshot.Id, gotSnapshot.Id) + + requireNoErr(mgr.DeleteInstance(ctx, sourceID)) + sourceDeleted = true + + _, err = mgr.GetSnapshot(ctx, snapshot.Id) + requireNoErr(err) + + forked, err := mgr.ForkSnapshot(ctx, snapshot.Id, ForkSnapshotRequest{ + Name: cfg.forkName, + TargetState: StateStandby, + }) + requireNoErr(err) + require.Equal(t, StateStandby, forked.State) + + forkID := forked.Id + t.Cleanup(func() { _ = mgr.DeleteInstance(context.Background(), forkID) }) + currentFork, err := mgr.GetInstance(ctx, forkID) + requireNoErr(err) + require.Equal(t, StateStandby, currentFork.State) +} diff --git a/lib/instances/snapshot_test.go b/lib/instances/snapshot_test.go new file mode 100644 index 00000000..f43a85c6 --- /dev/null +++ b/lib/instances/snapshot_test.go @@ -0,0 +1,115 @@ +package instances + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { + mgr, _ := setupTestManager(t) + ctx := context.Background() + + hvType := mgr.defaultHypervisor + sourceID := "snapshot-stopped-src" + createStoppedSnapshotSourceFixture(t, mgr, sourceID, "snapshot-stopped-src", hvType) + + snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStopped, + Name: "stopped-baseline", + }) + require.NoError(t, err) + require.Equal(t, SnapshotKindStopped, snap.Kind) + + restored, err := mgr.RestoreSnapshot(ctx, sourceID, snap.Id, RestoreSnapshotRequest{ + TargetState: StateStopped, + TargetHypervisor: hvType, + }) + require.NoError(t, err) + require.Equal(t, StateStopped, restored.State) + require.Equal(t, hvType, restored.HypervisorType) + + require.NoError(t, mgr.DeleteInstance(ctx, sourceID)) + + got, err := mgr.GetSnapshot(ctx, snap.Id) + require.NoError(t, err) + require.Equal(t, snap.Id, got.Id) + + forked, err := mgr.ForkSnapshot(ctx, snap.Id, ForkSnapshotRequest{ + Name: "snapshot-stopped-fork", + TargetState: StateStopped, + TargetHypervisor: hvType, + }) + require.NoError(t, err) + require.Equal(t, StateStopped, forked.State) + require.Equal(t, hvType, forked.HypervisorType) + t.Cleanup(func() { _ = mgr.DeleteInstance(context.Background(), forked.Id) }) + + require.NoError(t, mgr.DeleteSnapshot(ctx, snap.Id)) + _, err = mgr.GetSnapshot(ctx, snap.Id) + require.Error(t, err) + assert.ErrorIs(t, err, ErrSnapshotNotFound) +} + +func TestStandbySnapshotRejectsTargetHypervisorOverride(t *testing.T) { + mgr, _ := setupTestManager(t) + ctx := context.Background() + + hvType := mgr.defaultHypervisor + sourceID := "snapshot-standby-src" + createStandbySnapshotSourceFixture(t, mgr, sourceID, "snapshot-standby-src", hvType) + + snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStandby, + Name: "standby-baseline", + }) + require.NoError(t, err) + + _, err = mgr.RestoreSnapshot(ctx, sourceID, snap.Id, RestoreSnapshotRequest{ + TargetState: StateStandby, + TargetHypervisor: hvType, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidRequest) +} + +func createStoppedSnapshotSourceFixture(t *testing.T, mgr *manager, id, name string, hvType hypervisor.Type) { + t.Helper() + require.NoError(t, mgr.ensureDirectories(id)) + + starter, err := mgr.getVMStarter(hvType) + require.NoError(t, err) + + now := time.Now() + meta := &metadata{StoredMetadata: StoredMetadata{ + Id: id, + Name: name, + Image: "docker.io/library/alpine:latest", + CreatedAt: now, + StoppedAt: &now, + HypervisorType: hvType, + HypervisorVersion: "test", + SocketPath: mgr.paths.InstanceSocket(id, starter.SocketName()), + DataDir: mgr.paths.InstanceDir(id), + VsockCID: generateVsockCID(id), + VsockSocket: mgr.paths.InstanceSocket(id, hypervisor.VsockSocketNameForType(hvType)), + NetworkEnabled: false, + }} + require.NoError(t, mgr.saveMetadata(meta)) + require.NoError(t, os.WriteFile(mgr.paths.InstanceOverlay(id), []byte("overlay"), 0644)) + require.NoError(t, os.WriteFile(mgr.paths.InstanceConfigDisk(id), []byte("config"), 0644)) +} + +func createStandbySnapshotSourceFixture(t *testing.T, mgr *manager, id, name string, hvType hypervisor.Type) { + t.Helper() + createStoppedSnapshotSourceFixture(t, mgr, id, name, hvType) + snapshotDir := mgr.paths.InstanceSnapshotLatest(id) + require.NoError(t, os.MkdirAll(snapshotDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(snapshotDir, "state"), []byte("snapshot"), 0644)) +} diff --git a/lib/instances/types.go b/lib/instances/types.go index f7679285..6c373302 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -4,6 +4,7 @@ import ( "time" "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/snapshot" ) // State represents the instance state @@ -182,6 +183,41 @@ type ForkInstanceRequest struct { TargetState State // Optional: desired final state of forked instance (Stopped, Standby, Running). Empty means inherit source state. } +// SnapshotKind determines how snapshot data is captured and restored. +type SnapshotKind = snapshot.SnapshotKind + +const ( + // SnapshotKindStandby captures snapshot-based standby state (memory/device/disk). + SnapshotKindStandby = snapshot.SnapshotKindStandby + // SnapshotKindStopped captures stopped-state disk+metadata only. + SnapshotKindStopped = snapshot.SnapshotKindStopped +) + +// Snapshot is a centrally stored immutable snapshot resource. +type Snapshot = snapshot.Snapshot + +// ListSnapshotsFilter contains optional filters for listing snapshots. +type ListSnapshotsFilter = snapshot.ListSnapshotsFilter + +// CreateSnapshotRequest is the domain request for creating a snapshot. +type CreateSnapshotRequest struct { + Kind SnapshotKind // Required: Standby or Stopped + Name string // Optional: unique per source instance +} + +// RestoreSnapshotRequest is the domain request for restoring a snapshot in-place. +type RestoreSnapshotRequest struct { + TargetState State // Optional + TargetHypervisor hypervisor.Type // Optional, allowed only for Stopped snapshots +} + +// ForkSnapshotRequest is the domain request for forking from a snapshot. +type ForkSnapshotRequest struct { + Name string // Required: name for the new instance + TargetState State // Optional + TargetHypervisor hypervisor.Type // Optional, allowed only for Stopped snapshots +} + // AttachVolumeRequest is the domain request for attaching a volume (used for API compatibility) type AttachVolumeRequest struct { MountPath string diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 8c754bb2..1121099b 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -60,6 +60,14 @@ const ( Pci DeviceType = "pci" ) +// Defines values for ForkSnapshotRequestTargetHypervisor. +const ( + ForkSnapshotRequestTargetHypervisorCloudHypervisor ForkSnapshotRequestTargetHypervisor = "cloud-hypervisor" + ForkSnapshotRequestTargetHypervisorFirecracker ForkSnapshotRequestTargetHypervisor = "firecracker" + ForkSnapshotRequestTargetHypervisorQemu ForkSnapshotRequestTargetHypervisor = "qemu" + ForkSnapshotRequestTargetHypervisorVz ForkSnapshotRequestTargetHypervisor = "vz" +) + // Defines values for ForkTargetState. const ( ForkTargetStateRunning ForkTargetState = "Running" @@ -106,6 +114,35 @@ const ( InstanceStateUnknown InstanceState = "Unknown" ) +// Defines values for RestoreSnapshotRequestTargetHypervisor. +const ( + RestoreSnapshotRequestTargetHypervisorCloudHypervisor RestoreSnapshotRequestTargetHypervisor = "cloud-hypervisor" + RestoreSnapshotRequestTargetHypervisorFirecracker RestoreSnapshotRequestTargetHypervisor = "firecracker" + RestoreSnapshotRequestTargetHypervisorQemu RestoreSnapshotRequestTargetHypervisor = "qemu" + RestoreSnapshotRequestTargetHypervisorVz RestoreSnapshotRequestTargetHypervisor = "vz" +) + +// Defines values for SnapshotSourceHypervisor. +const ( + CloudHypervisor SnapshotSourceHypervisor = "cloud-hypervisor" + Firecracker SnapshotSourceHypervisor = "firecracker" + Qemu SnapshotSourceHypervisor = "qemu" + Vz SnapshotSourceHypervisor = "vz" +) + +// Defines values for SnapshotKind. +const ( + SnapshotKindStandby SnapshotKind = "Standby" + SnapshotKindStopped SnapshotKind = "Stopped" +) + +// Defines values for SnapshotTargetState. +const ( + SnapshotTargetStateRunning SnapshotTargetState = "Running" + SnapshotTargetStateStandby SnapshotTargetState = "Standby" + SnapshotTargetStateStopped SnapshotTargetState = "Stopped" +) + // Defines values for GetInstanceLogsParamsSource. const ( App GetInstanceLogsParamsSource = "app" @@ -320,6 +357,15 @@ type CreateInstanceRequest struct { // CreateInstanceRequestHypervisor Hypervisor to use for this instance. Defaults to server configuration. type CreateInstanceRequestHypervisor string +// CreateSnapshotRequest defines model for CreateSnapshotRequest. +type CreateSnapshotRequest struct { + // Kind Snapshot capture kind + Kind SnapshotKind `json:"kind"` + + // Name Optional snapshot name (lowercase letters, digits, and dashes only; cannot start or end with a dash) + Name *string `json:"name,omitempty"` +} + // CreateVolumeRequest defines model for CreateVolumeRequest. type CreateVolumeRequest struct { // Id Optional custom identifier (auto-generated if not provided) @@ -420,6 +466,23 @@ type ForkInstanceRequest struct { TargetState *ForkTargetState `json:"target_state,omitempty"` } +// ForkSnapshotRequest defines model for ForkSnapshotRequest. +type ForkSnapshotRequest struct { + // Name Name for the new instance (lowercase letters, digits, and dashes only; cannot start or end with a dash) + Name string `json:"name"` + + // TargetHypervisor Optional hypervisor override. Allowed only when forking from a Stopped snapshot. + // Standby snapshots must fork with their original hypervisor. + TargetHypervisor *ForkSnapshotRequestTargetHypervisor `json:"target_hypervisor,omitempty"` + + // TargetState Target state when restoring or forking from a snapshot + TargetState *SnapshotTargetState `json:"target_state,omitempty"` +} + +// ForkSnapshotRequestTargetHypervisor Optional hypervisor override. Allowed only when forking from a Stopped snapshot. +// Standby snapshots must fork with their original hypervisor. +type ForkSnapshotRequestTargetHypervisor string + // ForkTargetState Target state for the forked instance after fork completes type ForkTargetState string @@ -814,6 +877,56 @@ type Resources struct { Network ResourceStatus `json:"network"` } +// RestoreSnapshotRequest defines model for RestoreSnapshotRequest. +type RestoreSnapshotRequest struct { + // TargetHypervisor Optional hypervisor override. Allowed only when restoring from a Stopped snapshot. + // Standby snapshots must restore with their original hypervisor. + TargetHypervisor *RestoreSnapshotRequestTargetHypervisor `json:"target_hypervisor,omitempty"` + + // TargetState Target state when restoring or forking from a snapshot + TargetState *SnapshotTargetState `json:"target_state,omitempty"` +} + +// RestoreSnapshotRequestTargetHypervisor Optional hypervisor override. Allowed only when restoring from a Stopped snapshot. +// Standby snapshots must restore with their original hypervisor. +type RestoreSnapshotRequestTargetHypervisor string + +// Snapshot defines model for Snapshot. +type Snapshot struct { + // CreatedAt Snapshot creation timestamp + CreatedAt time.Time `json:"created_at"` + + // Id Auto-generated unique snapshot identifier + Id string `json:"id"` + + // Kind Snapshot capture kind + Kind SnapshotKind `json:"kind"` + + // Name Optional human-readable snapshot name (unique per source instance) + Name *string `json:"name"` + + // SizeBytes Total payload size in bytes + SizeBytes int64 `json:"size_bytes"` + + // SourceHypervisor Source instance hypervisor at snapshot creation time + SourceHypervisor SnapshotSourceHypervisor `json:"source_hypervisor"` + + // SourceInstanceId Source instance ID at snapshot creation time + SourceInstanceId string `json:"source_instance_id"` + + // SourceInstanceName Source instance name at snapshot creation time + SourceInstanceName string `json:"source_instance_name"` +} + +// SnapshotSourceHypervisor Source instance hypervisor at snapshot creation time +type SnapshotSourceHypervisor string + +// SnapshotKind Snapshot capture kind +type SnapshotKind string + +// SnapshotTargetState Target state when restoring or forking from a snapshot +type SnapshotTargetState string + // Volume defines model for Volume. type Volume struct { // Attachments List of current attachments (empty if not attached) @@ -957,6 +1070,18 @@ type StatInstancePathParams struct { FollowLinks *bool `form:"follow_links,omitempty" json:"follow_links,omitempty"` } +// ListSnapshotsParams defines parameters for ListSnapshots. +type ListSnapshotsParams struct { + // SourceInstanceId Filter snapshots by source instance ID + SourceInstanceId *string `form:"source_instance_id,omitempty" json:"source_instance_id,omitempty"` + + // Kind Filter snapshots by kind + Kind *SnapshotKind `form:"kind,omitempty" json:"kind,omitempty"` + + // Name Filter snapshots by snapshot name + Name *string `form:"name,omitempty" json:"name,omitempty"` +} + // CreateVolumeFromArchiveParams defines parameters for CreateVolumeFromArchive. type CreateVolumeFromArchiveParams struct { // Name Volume name @@ -987,12 +1112,21 @@ type CreateInstanceJSONRequestBody = CreateInstanceRequest // ForkInstanceJSONRequestBody defines body for ForkInstance for application/json ContentType. type ForkInstanceJSONRequestBody = ForkInstanceRequest +// CreateInstanceSnapshotJSONRequestBody defines body for CreateInstanceSnapshot for application/json ContentType. +type CreateInstanceSnapshotJSONRequestBody = CreateSnapshotRequest + +// RestoreInstanceSnapshotJSONRequestBody defines body for RestoreInstanceSnapshot for application/json ContentType. +type RestoreInstanceSnapshotJSONRequestBody = RestoreSnapshotRequest + // StartInstanceJSONRequestBody defines body for StartInstance for application/json ContentType. type StartInstanceJSONRequestBody StartInstanceJSONBody // AttachVolumeJSONRequestBody defines body for AttachVolume for application/json ContentType. type AttachVolumeJSONRequestBody = AttachVolumeRequest +// ForkSnapshotJSONRequestBody defines body for ForkSnapshot for application/json ContentType. +type ForkSnapshotJSONRequestBody = ForkSnapshotRequest + // CreateVolumeJSONRequestBody defines body for CreateVolume for application/json ContentType. type CreateVolumeJSONRequestBody = CreateVolumeRequest @@ -1157,6 +1291,16 @@ type ClientInterface interface { // RestoreInstance request RestoreInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateInstanceSnapshotWithBody request with any body + CreateInstanceSnapshotWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateInstanceSnapshot(ctx context.Context, id string, body CreateInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RestoreInstanceSnapshotWithBody request with any body + RestoreInstanceSnapshotWithBody(ctx context.Context, id string, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RestoreInstanceSnapshot(ctx context.Context, id string, snapshotId string, body RestoreInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // StandbyInstance request StandbyInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1185,6 +1329,20 @@ type ClientInterface interface { // GetResources request GetResources(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListSnapshots request + ListSnapshots(ctx context.Context, params *ListSnapshotsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteSnapshot request + DeleteSnapshot(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetSnapshot request + GetSnapshot(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ForkSnapshotWithBody request with any body + ForkSnapshotWithBody(ctx context.Context, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ForkSnapshot(ctx context.Context, snapshotId string, body ForkSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListVolumes request ListVolumes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1575,6 +1733,54 @@ func (c *Client) RestoreInstance(ctx context.Context, id string, reqEditors ...R return c.Client.Do(req) } +func (c *Client) CreateInstanceSnapshotWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateInstanceSnapshotRequestWithBody(c.Server, id, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateInstanceSnapshot(ctx context.Context, id string, body CreateInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateInstanceSnapshotRequest(c.Server, id, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RestoreInstanceSnapshotWithBody(ctx context.Context, id string, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRestoreInstanceSnapshotRequestWithBody(c.Server, id, snapshotId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RestoreInstanceSnapshot(ctx context.Context, id string, snapshotId string, body RestoreInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRestoreInstanceSnapshotRequest(c.Server, id, snapshotId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) StandbyInstance(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewStandbyInstanceRequest(c.Server, id) if err != nil { @@ -1695,6 +1901,66 @@ func (c *Client) GetResources(ctx context.Context, reqEditors ...RequestEditorFn return c.Client.Do(req) } +func (c *Client) ListSnapshots(ctx context.Context, params *ListSnapshotsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListSnapshotsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteSnapshot(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteSnapshotRequest(c.Server, snapshotId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetSnapshot(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetSnapshotRequest(c.Server, snapshotId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ForkSnapshotWithBody(ctx context.Context, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewForkSnapshotRequestWithBody(c.Server, snapshotId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ForkSnapshot(ctx context.Context, snapshotId string, body ForkSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewForkSnapshotRequest(c.Server, snapshotId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ListVolumes(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewListVolumesRequest(c.Server) if err != nil { @@ -2748,6 +3014,107 @@ func NewRestoreInstanceRequest(server string, id string) (*http.Request, error) return req, nil } +// NewCreateInstanceSnapshotRequest calls the generic CreateInstanceSnapshot builder with application/json body +func NewCreateInstanceSnapshotRequest(server string, id string, body CreateInstanceSnapshotJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateInstanceSnapshotRequestWithBody(server, id, "application/json", bodyReader) +} + +// NewCreateInstanceSnapshotRequestWithBody generates requests for CreateInstanceSnapshot with any type of body +func NewCreateInstanceSnapshotRequestWithBody(server string, id string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/instances/%s/snapshots", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewRestoreInstanceSnapshotRequest calls the generic RestoreInstanceSnapshot builder with application/json body +func NewRestoreInstanceSnapshotRequest(server string, id string, snapshotId string, body RestoreInstanceSnapshotJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRestoreInstanceSnapshotRequestWithBody(server, id, snapshotId, "application/json", bodyReader) +} + +// NewRestoreInstanceSnapshotRequestWithBody generates requests for RestoreInstanceSnapshot with any type of body +func NewRestoreInstanceSnapshotRequestWithBody(server string, id string, snapshotId string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "snapshotId", runtime.ParamLocationPath, snapshotId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/instances/%s/snapshots/%s/restore", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewStandbyInstanceRequest generates requests for StandbyInstance func NewStandbyInstanceRequest(server string, id string) (*http.Request, error) { var err error @@ -3087,8 +3454,8 @@ func NewGetResourcesRequest(server string) (*http.Request, error) { return req, nil } -// NewListVolumesRequest generates requests for ListVolumes -func NewListVolumesRequest(server string) (*http.Request, error) { +// NewListSnapshotsRequest generates requests for ListSnapshots +func NewListSnapshotsRequest(server string, params *ListSnapshotsParams) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -3096,7 +3463,7 @@ func NewListVolumesRequest(server string) (*http.Request, error) { return nil, err } - operationPath := fmt.Sprintf("/volumes") + operationPath := fmt.Sprintf("/snapshots") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3106,22 +3473,218 @@ func NewListVolumesRequest(server string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) - if err != nil { - return nil, err - } + if params != nil { + queryValues := queryURL.Query() - return req, nil -} + if params.SourceInstanceId != nil { -// NewCreateVolumeRequest calls the generic CreateVolume builder with application/json body -func NewCreateVolumeRequest(server string, body CreateVolumeJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source_instance_id", runtime.ParamLocationQuery, *params.SourceInstanceId); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Kind != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "kind", runtime.ParamLocationQuery, *params.Kind); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Name != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, *params.Name); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteSnapshotRequest generates requests for DeleteSnapshot +func NewDeleteSnapshotRequest(server string, snapshotId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "snapshotId", runtime.ParamLocationPath, snapshotId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/snapshots/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetSnapshotRequest generates requests for GetSnapshot +func NewGetSnapshotRequest(server string, snapshotId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "snapshotId", runtime.ParamLocationPath, snapshotId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/snapshots/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewForkSnapshotRequest calls the generic ForkSnapshot builder with application/json body +func NewForkSnapshotRequest(server string, snapshotId string, body ForkSnapshotJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewForkSnapshotRequestWithBody(server, snapshotId, "application/json", bodyReader) +} + +// NewForkSnapshotRequestWithBody generates requests for ForkSnapshot with any type of body +func NewForkSnapshotRequestWithBody(server string, snapshotId string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "snapshotId", runtime.ParamLocationPath, snapshotId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/snapshots/%s/fork", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewListVolumesRequest generates requests for ListVolumes +func NewListVolumesRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/volumes") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateVolumeRequest calls the generic CreateVolume builder with application/json body +func NewCreateVolumeRequest(server string, body CreateVolumeJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) return NewCreateVolumeRequestWithBody(server, "application/json", bodyReader) } @@ -3428,6 +3991,16 @@ type ClientWithResponsesInterface interface { // RestoreInstanceWithResponse request RestoreInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*RestoreInstanceResponse, error) + // CreateInstanceSnapshotWithBodyWithResponse request with any body + CreateInstanceSnapshotWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateInstanceSnapshotResponse, error) + + CreateInstanceSnapshotWithResponse(ctx context.Context, id string, body CreateInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateInstanceSnapshotResponse, error) + + // RestoreInstanceSnapshotWithBodyWithResponse request with any body + RestoreInstanceSnapshotWithBodyWithResponse(ctx context.Context, id string, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RestoreInstanceSnapshotResponse, error) + + RestoreInstanceSnapshotWithResponse(ctx context.Context, id string, snapshotId string, body RestoreInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*RestoreInstanceSnapshotResponse, error) + // StandbyInstanceWithResponse request StandbyInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*StandbyInstanceResponse, error) @@ -3456,6 +4029,20 @@ type ClientWithResponsesInterface interface { // GetResourcesWithResponse request GetResourcesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetResourcesResponse, error) + // ListSnapshotsWithResponse request + ListSnapshotsWithResponse(ctx context.Context, params *ListSnapshotsParams, reqEditors ...RequestEditorFn) (*ListSnapshotsResponse, error) + + // DeleteSnapshotWithResponse request + DeleteSnapshotWithResponse(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*DeleteSnapshotResponse, error) + + // GetSnapshotWithResponse request + GetSnapshotWithResponse(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*GetSnapshotResponse, error) + + // ForkSnapshotWithBodyWithResponse request with any body + ForkSnapshotWithBodyWithResponse(ctx context.Context, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ForkSnapshotResponse, error) + + ForkSnapshotWithResponse(ctx context.Context, snapshotId string, body ForkSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*ForkSnapshotResponse, error) + // ListVolumesWithResponse request ListVolumesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListVolumesResponse, error) @@ -4107,6 +4694,60 @@ func (r RestoreInstanceResponse) StatusCode() int { return 0 } +type CreateInstanceSnapshotResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Snapshot + JSON400 *Error + JSON404 *Error + JSON409 *Error + JSON500 *Error + JSON501 *Error +} + +// Status returns HTTPResponse.Status +func (r CreateInstanceSnapshotResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateInstanceSnapshotResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RestoreInstanceSnapshotResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Instance + JSON400 *Error + JSON404 *Error + JSON409 *Error + JSON500 *Error + JSON501 *Error +} + +// Status returns HTTPResponse.Status +func (r RestoreInstanceSnapshotResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RestoreInstanceSnapshotResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type StandbyInstanceResponse struct { Body []byte HTTPResponse *http.Response @@ -4303,16 +4944,15 @@ func (r GetResourcesResponse) StatusCode() int { return 0 } -type ListVolumesResponse struct { +type ListSnapshotsResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *[]Volume - JSON401 *Error + JSON200 *[]Snapshot JSON500 *Error } // Status returns HTTPResponse.Status -func (r ListVolumesResponse) Status() string { +func (r ListSnapshotsResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4320,25 +4960,22 @@ func (r ListVolumesResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ListVolumesResponse) StatusCode() int { +func (r ListSnapshotsResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type CreateVolumeResponse struct { +type DeleteSnapshotResponse struct { Body []byte HTTPResponse *http.Response - JSON201 *Volume - JSON400 *Error - JSON401 *Error - JSON409 *Error + JSON404 *Error JSON500 *Error } // Status returns HTTPResponse.Status -func (r CreateVolumeResponse) Status() string { +func (r DeleteSnapshotResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4346,7 +4983,108 @@ func (r CreateVolumeResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r CreateVolumeResponse) StatusCode() int { +func (r DeleteSnapshotResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetSnapshotResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Snapshot + JSON404 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r GetSnapshotResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetSnapshotResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ForkSnapshotResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Instance + JSON400 *Error + JSON404 *Error + JSON409 *Error + JSON500 *Error + JSON501 *Error +} + +// Status returns HTTPResponse.Status +func (r ForkSnapshotResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ForkSnapshotResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ListVolumesResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]Volume + JSON401 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r ListVolumesResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListVolumesResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreateVolumeResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Volume + JSON400 *Error + JSON401 *Error + JSON409 *Error + JSON500 *Error +} + +// Status returns HTTPResponse.Status +func (r CreateVolumeResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateVolumeResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -4701,6 +5439,40 @@ func (c *ClientWithResponses) RestoreInstanceWithResponse(ctx context.Context, i return ParseRestoreInstanceResponse(rsp) } +// CreateInstanceSnapshotWithBodyWithResponse request with arbitrary body returning *CreateInstanceSnapshotResponse +func (c *ClientWithResponses) CreateInstanceSnapshotWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateInstanceSnapshotResponse, error) { + rsp, err := c.CreateInstanceSnapshotWithBody(ctx, id, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateInstanceSnapshotResponse(rsp) +} + +func (c *ClientWithResponses) CreateInstanceSnapshotWithResponse(ctx context.Context, id string, body CreateInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateInstanceSnapshotResponse, error) { + rsp, err := c.CreateInstanceSnapshot(ctx, id, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateInstanceSnapshotResponse(rsp) +} + +// RestoreInstanceSnapshotWithBodyWithResponse request with arbitrary body returning *RestoreInstanceSnapshotResponse +func (c *ClientWithResponses) RestoreInstanceSnapshotWithBodyWithResponse(ctx context.Context, id string, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RestoreInstanceSnapshotResponse, error) { + rsp, err := c.RestoreInstanceSnapshotWithBody(ctx, id, snapshotId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRestoreInstanceSnapshotResponse(rsp) +} + +func (c *ClientWithResponses) RestoreInstanceSnapshotWithResponse(ctx context.Context, id string, snapshotId string, body RestoreInstanceSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*RestoreInstanceSnapshotResponse, error) { + rsp, err := c.RestoreInstanceSnapshot(ctx, id, snapshotId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRestoreInstanceSnapshotResponse(rsp) +} + // StandbyInstanceWithResponse request returning *StandbyInstanceResponse func (c *ClientWithResponses) StandbyInstanceWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*StandbyInstanceResponse, error) { rsp, err := c.StandbyInstance(ctx, id, reqEditors...) @@ -4789,6 +5561,50 @@ func (c *ClientWithResponses) GetResourcesWithResponse(ctx context.Context, reqE return ParseGetResourcesResponse(rsp) } +// ListSnapshotsWithResponse request returning *ListSnapshotsResponse +func (c *ClientWithResponses) ListSnapshotsWithResponse(ctx context.Context, params *ListSnapshotsParams, reqEditors ...RequestEditorFn) (*ListSnapshotsResponse, error) { + rsp, err := c.ListSnapshots(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseListSnapshotsResponse(rsp) +} + +// DeleteSnapshotWithResponse request returning *DeleteSnapshotResponse +func (c *ClientWithResponses) DeleteSnapshotWithResponse(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*DeleteSnapshotResponse, error) { + rsp, err := c.DeleteSnapshot(ctx, snapshotId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteSnapshotResponse(rsp) +} + +// GetSnapshotWithResponse request returning *GetSnapshotResponse +func (c *ClientWithResponses) GetSnapshotWithResponse(ctx context.Context, snapshotId string, reqEditors ...RequestEditorFn) (*GetSnapshotResponse, error) { + rsp, err := c.GetSnapshot(ctx, snapshotId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetSnapshotResponse(rsp) +} + +// ForkSnapshotWithBodyWithResponse request with arbitrary body returning *ForkSnapshotResponse +func (c *ClientWithResponses) ForkSnapshotWithBodyWithResponse(ctx context.Context, snapshotId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ForkSnapshotResponse, error) { + rsp, err := c.ForkSnapshotWithBody(ctx, snapshotId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseForkSnapshotResponse(rsp) +} + +func (c *ClientWithResponses) ForkSnapshotWithResponse(ctx context.Context, snapshotId string, body ForkSnapshotJSONRequestBody, reqEditors ...RequestEditorFn) (*ForkSnapshotResponse, error) { + rsp, err := c.ForkSnapshot(ctx, snapshotId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseForkSnapshotResponse(rsp) +} + // ListVolumesWithResponse request returning *ListVolumesResponse func (c *ClientWithResponses) ListVolumesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListVolumesResponse, error) { rsp, err := c.ListVolumes(ctx, reqEditors...) @@ -5830,7 +6646,357 @@ func ParseForkInstanceResponse(rsp *http.Response) (*ForkInstanceResponse, error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 501: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON501 = &dest + + } + + return response, nil +} + +// ParseGetInstanceLogsResponse parses an HTTP response from a GetInstanceLogsWithResponse call +func ParseGetInstanceLogsResponse(rsp *http.Response) (*GetInstanceLogsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetInstanceLogsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseRestoreInstanceResponse parses an HTTP response from a RestoreInstanceWithResponse call +func ParseRestoreInstanceResponse(rsp *http.Response) (*RestoreInstanceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RestoreInstanceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Instance + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseCreateInstanceSnapshotResponse parses an HTTP response from a CreateInstanceSnapshotWithResponse call +func ParseCreateInstanceSnapshotResponse(rsp *http.Response) (*CreateInstanceSnapshotResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateInstanceSnapshotResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Snapshot + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 501: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON501 = &dest + + } + + return response, nil +} + +// ParseRestoreInstanceSnapshotResponse parses an HTTP response from a RestoreInstanceSnapshotWithResponse call +func ParseRestoreInstanceSnapshotResponse(rsp *http.Response) (*RestoreInstanceSnapshotResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RestoreInstanceSnapshotResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Instance + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 501: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON501 = &dest + + } + + return response, nil +} + +// ParseStandbyInstanceResponse parses an HTTP response from a StandbyInstanceWithResponse call +func ParseStandbyInstanceResponse(rsp *http.Response) (*StandbyInstanceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StandbyInstanceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Instance + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseStartInstanceResponse parses an HTTP response from a StartInstanceWithResponse call +func ParseStartInstanceResponse(rsp *http.Response) (*StartInstanceResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StartInstanceResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Instance + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseStatInstancePathResponse parses an HTTP response from a StatInstancePathWithResponse call +func ParseStatInstancePathResponse(rsp *http.Response) (*StatInstancePathResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StatInstancePathResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PathInfo + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -5853,32 +7019,32 @@ func ParseForkInstanceResponse(rsp *http.Response) (*ForkInstanceResponse, error } response.JSON500 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 501: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON501 = &dest - } return response, nil } -// ParseGetInstanceLogsResponse parses an HTTP response from a GetInstanceLogsWithResponse call -func ParseGetInstanceLogsResponse(rsp *http.Response) (*GetInstanceLogsResponse, error) { +// ParseGetInstanceStatsResponse parses an HTTP response from a GetInstanceStatsWithResponse call +func ParseGetInstanceStatsResponse(rsp *http.Response) (*GetInstanceStatsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetInstanceLogsResponse{ + response := &GetInstanceStatsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest InstanceStats + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5898,15 +7064,15 @@ func ParseGetInstanceLogsResponse(rsp *http.Response) (*GetInstanceLogsResponse, return response, nil } -// ParseRestoreInstanceResponse parses an HTTP response from a RestoreInstanceWithResponse call -func ParseRestoreInstanceResponse(rsp *http.Response) (*RestoreInstanceResponse, error) { +// ParseStopInstanceResponse parses an HTTP response from a StopInstanceWithResponse call +func ParseStopInstanceResponse(rsp *http.Response) (*StopInstanceResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &RestoreInstanceResponse{ + response := &StopInstanceResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5945,15 +7111,15 @@ func ParseRestoreInstanceResponse(rsp *http.Response) (*RestoreInstanceResponse, return response, nil } -// ParseStandbyInstanceResponse parses an HTTP response from a StandbyInstanceWithResponse call -func ParseStandbyInstanceResponse(rsp *http.Response) (*StandbyInstanceResponse, error) { +// ParseDetachVolumeResponse parses an HTTP response from a DetachVolumeWithResponse call +func ParseDetachVolumeResponse(rsp *http.Response) (*DetachVolumeResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StandbyInstanceResponse{ + response := &DetachVolumeResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5973,13 +7139,6 @@ func ParseStandbyInstanceResponse(rsp *http.Response) (*StandbyInstanceResponse, } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5992,15 +7151,15 @@ func ParseStandbyInstanceResponse(rsp *http.Response) (*StandbyInstanceResponse, return response, nil } -// ParseStartInstanceResponse parses an HTTP response from a StartInstanceWithResponse call -func ParseStartInstanceResponse(rsp *http.Response) (*StartInstanceResponse, error) { +// ParseAttachVolumeResponse parses an HTTP response from a AttachVolumeWithResponse call +func ParseAttachVolumeResponse(rsp *http.Response) (*AttachVolumeResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StartInstanceResponse{ + response := &AttachVolumeResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -6039,41 +7198,27 @@ func ParseStartInstanceResponse(rsp *http.Response) (*StartInstanceResponse, err return response, nil } -// ParseStatInstancePathResponse parses an HTTP response from a StatInstancePathWithResponse call -func ParseStatInstancePathResponse(rsp *http.Response) (*StatInstancePathResponse, error) { +// ParseGetResourcesResponse parses an HTTP response from a GetResourcesWithResponse call +func ParseGetResourcesResponse(rsp *http.Response) (*GetResourcesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StatInstancePathResponse{ + response := &GetResourcesResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PathInfo + var dest Resources if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6086,34 +7231,27 @@ func ParseStatInstancePathResponse(rsp *http.Response) (*StatInstancePathRespons return response, nil } -// ParseGetInstanceStatsResponse parses an HTTP response from a GetInstanceStatsWithResponse call -func ParseGetInstanceStatsResponse(rsp *http.Response) (*GetInstanceStatsResponse, error) { +// ParseListSnapshotsResponse parses an HTTP response from a ListSnapshotsWithResponse call +func ParseListSnapshotsResponse(rsp *http.Response) (*ListSnapshotsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetInstanceStatsResponse{ + response := &ListSnapshotsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest InstanceStats + var dest []Snapshot if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6126,27 +7264,20 @@ func ParseGetInstanceStatsResponse(rsp *http.Response) (*GetInstanceStatsRespons return response, nil } -// ParseStopInstanceResponse parses an HTTP response from a StopInstanceWithResponse call -func ParseStopInstanceResponse(rsp *http.Response) (*StopInstanceResponse, error) { +// ParseDeleteSnapshotResponse parses an HTTP response from a DeleteSnapshotWithResponse call +func ParseDeleteSnapshotResponse(rsp *http.Response) (*DeleteSnapshotResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StopInstanceResponse{ + response := &DeleteSnapshotResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Instance - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6154,13 +7285,6 @@ func ParseStopInstanceResponse(rsp *http.Response) (*StopInstanceResponse, error } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6173,22 +7297,22 @@ func ParseStopInstanceResponse(rsp *http.Response) (*StopInstanceResponse, error return response, nil } -// ParseDetachVolumeResponse parses an HTTP response from a DetachVolumeWithResponse call -func ParseDetachVolumeResponse(rsp *http.Response) (*DetachVolumeResponse, error) { +// ParseGetSnapshotResponse parses an HTTP response from a GetSnapshotWithResponse call +func ParseGetSnapshotResponse(rsp *http.Response) (*GetSnapshotResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DetachVolumeResponse{ + response := &GetSnapshotResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Instance + var dest Snapshot if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -6213,26 +7337,33 @@ func ParseDetachVolumeResponse(rsp *http.Response) (*DetachVolumeResponse, error return response, nil } -// ParseAttachVolumeResponse parses an HTTP response from a AttachVolumeWithResponse call -func ParseAttachVolumeResponse(rsp *http.Response) (*AttachVolumeResponse, error) { +// ParseForkSnapshotResponse parses an HTTP response from a ForkSnapshotWithResponse call +func ParseForkSnapshotResponse(rsp *http.Response) (*ForkSnapshotResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &AttachVolumeResponse{ + response := &ForkSnapshotResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: var dest Instance if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -6255,38 +7386,12 @@ func ParseAttachVolumeResponse(rsp *http.Response) (*AttachVolumeResponse, error } response.JSON500 = &dest - } - - return response, nil -} - -// ParseGetResourcesResponse parses an HTTP response from a GetResourcesWithResponse call -func ParseGetResourcesResponse(rsp *http.Response) (*GetResourcesResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &GetResourcesResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Resources - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 501: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSON501 = &dest } @@ -6601,6 +7706,12 @@ type ServerInterface interface { // Restore instance from standby // (POST /instances/{id}/restore) RestoreInstance(w http.ResponseWriter, r *http.Request, id string) + // Create a snapshot for an instance + // (POST /instances/{id}/snapshots) + CreateInstanceSnapshot(w http.ResponseWriter, r *http.Request, id string) + // Restore an instance from a snapshot in-place + // (POST /instances/{id}/snapshots/{snapshotId}/restore) + RestoreInstanceSnapshot(w http.ResponseWriter, r *http.Request, id string, snapshotId string) // Put instance in standby (pause, snapshot, delete VMM) // (POST /instances/{id}/standby) StandbyInstance(w http.ResponseWriter, r *http.Request, id string) @@ -6625,6 +7736,18 @@ type ServerInterface interface { // Get host resource capacity and allocations // (GET /resources) GetResources(w http.ResponseWriter, r *http.Request) + // List snapshots + // (GET /snapshots) + ListSnapshots(w http.ResponseWriter, r *http.Request, params ListSnapshotsParams) + // Delete a snapshot + // (DELETE /snapshots/{snapshotId}) + DeleteSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) + // Get snapshot details + // (GET /snapshots/{snapshotId}) + GetSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) + // Fork a new instance from a snapshot + // (POST /snapshots/{snapshotId}/fork) + ForkSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) // List volumes // (GET /volumes) ListVolumes(w http.ResponseWriter, r *http.Request) @@ -6802,6 +7925,18 @@ func (_ Unimplemented) RestoreInstance(w http.ResponseWriter, r *http.Request, i w.WriteHeader(http.StatusNotImplemented) } +// Create a snapshot for an instance +// (POST /instances/{id}/snapshots) +func (_ Unimplemented) CreateInstanceSnapshot(w http.ResponseWriter, r *http.Request, id string) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Restore an instance from a snapshot in-place +// (POST /instances/{id}/snapshots/{snapshotId}/restore) +func (_ Unimplemented) RestoreInstanceSnapshot(w http.ResponseWriter, r *http.Request, id string, snapshotId string) { + w.WriteHeader(http.StatusNotImplemented) +} + // Put instance in standby (pause, snapshot, delete VMM) // (POST /instances/{id}/standby) func (_ Unimplemented) StandbyInstance(w http.ResponseWriter, r *http.Request, id string) { @@ -6850,6 +7985,30 @@ func (_ Unimplemented) GetResources(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// List snapshots +// (GET /snapshots) +func (_ Unimplemented) ListSnapshots(w http.ResponseWriter, r *http.Request, params ListSnapshotsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Delete a snapshot +// (DELETE /snapshots/{snapshotId}) +func (_ Unimplemented) DeleteSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get snapshot details +// (GET /snapshots/{snapshotId}) +func (_ Unimplemented) GetSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Fork a new instance from a snapshot +// (POST /snapshots/{snapshotId}/fork) +func (_ Unimplemented) ForkSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) { + w.WriteHeader(http.StatusNotImplemented) +} + // List volumes // (GET /volumes) func (_ Unimplemented) ListVolumes(w http.ResponseWriter, r *http.Request) { @@ -7496,8 +8655,97 @@ func (siw *ServerInterfaceWrapper) GetInstance(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } -// ForkInstance operation middleware -func (siw *ServerInterfaceWrapper) ForkInstance(w http.ResponseWriter, r *http.Request) { +// ForkInstance operation middleware +func (siw *ServerInterfaceWrapper) ForkInstance(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ForkInstance(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetInstanceLogs operation middleware +func (siw *ServerInterfaceWrapper) GetInstanceLogs(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetInstanceLogsParams + + // ------------- Optional query parameter "tail" ------------- + + err = runtime.BindQueryParameter("form", true, false, "tail", r.URL.Query(), ¶ms.Tail) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) + return + } + + // ------------- Optional query parameter "follow" ------------- + + err = runtime.BindQueryParameter("form", true, false, "follow", r.URL.Query(), ¶ms.Follow) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "follow", Err: err}) + return + } + + // ------------- Optional query parameter "source" ------------- + + err = runtime.BindQueryParameter("form", true, false, "source", r.URL.Query(), ¶ms.Source) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetInstanceLogs(w, r, id, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// RestoreInstance operation middleware +func (siw *ServerInterfaceWrapper) RestoreInstance(w http.ResponseWriter, r *http.Request) { var err error @@ -7517,7 +8765,7 @@ func (siw *ServerInterfaceWrapper) ForkInstance(w http.ResponseWriter, r *http.R r = r.WithContext(ctx) handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.ForkInstance(w, r, id) + siw.Handler.RestoreInstance(w, r, id) })) for _, middleware := range siw.HandlerMiddlewares { @@ -7527,8 +8775,8 @@ func (siw *ServerInterfaceWrapper) ForkInstance(w http.ResponseWriter, r *http.R handler.ServeHTTP(w, r) } -// GetInstanceLogs operation middleware -func (siw *ServerInterfaceWrapper) GetInstanceLogs(w http.ResponseWriter, r *http.Request) { +// CreateInstanceSnapshot operation middleware +func (siw *ServerInterfaceWrapper) CreateInstanceSnapshot(w http.ResponseWriter, r *http.Request) { var err error @@ -7547,35 +8795,8 @@ func (siw *ServerInterfaceWrapper) GetInstanceLogs(w http.ResponseWriter, r *htt r = r.WithContext(ctx) - // Parameter object where we will unmarshal all parameters from the context - var params GetInstanceLogsParams - - // ------------- Optional query parameter "tail" ------------- - - err = runtime.BindQueryParameter("form", true, false, "tail", r.URL.Query(), ¶ms.Tail) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "tail", Err: err}) - return - } - - // ------------- Optional query parameter "follow" ------------- - - err = runtime.BindQueryParameter("form", true, false, "follow", r.URL.Query(), ¶ms.Follow) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "follow", Err: err}) - return - } - - // ------------- Optional query parameter "source" ------------- - - err = runtime.BindQueryParameter("form", true, false, "source", r.URL.Query(), ¶ms.Source) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) - return - } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetInstanceLogs(w, r, id, params) + siw.Handler.CreateInstanceSnapshot(w, r, id) })) for _, middleware := range siw.HandlerMiddlewares { @@ -7585,8 +8806,8 @@ func (siw *ServerInterfaceWrapper) GetInstanceLogs(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } -// RestoreInstance operation middleware -func (siw *ServerInterfaceWrapper) RestoreInstance(w http.ResponseWriter, r *http.Request) { +// RestoreInstanceSnapshot operation middleware +func (siw *ServerInterfaceWrapper) RestoreInstanceSnapshot(w http.ResponseWriter, r *http.Request) { var err error @@ -7599,6 +8820,15 @@ func (siw *ServerInterfaceWrapper) RestoreInstance(w http.ResponseWriter, r *htt return } + // ------------- Path parameter "snapshotId" ------------- + var snapshotId string + + err = runtime.BindStyledParameterWithOptions("simple", "snapshotId", chi.URLParam(r, "snapshotId"), &snapshotId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "snapshotId", Err: err}) + return + } + ctx := r.Context() ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) @@ -7606,7 +8836,7 @@ func (siw *ServerInterfaceWrapper) RestoreInstance(w http.ResponseWriter, r *htt r = r.WithContext(ctx) handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.RestoreInstance(w, r, id) + siw.Handler.RestoreInstanceSnapshot(w, r, id, snapshotId) })) for _, middleware := range siw.HandlerMiddlewares { @@ -7897,6 +9127,148 @@ func (siw *ServerInterfaceWrapper) GetResources(w http.ResponseWriter, r *http.R handler.ServeHTTP(w, r) } +// ListSnapshots operation middleware +func (siw *ServerInterfaceWrapper) ListSnapshots(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params ListSnapshotsParams + + // ------------- Optional query parameter "source_instance_id" ------------- + + err = runtime.BindQueryParameter("form", true, false, "source_instance_id", r.URL.Query(), ¶ms.SourceInstanceId) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source_instance_id", Err: err}) + return + } + + // ------------- Optional query parameter "kind" ------------- + + err = runtime.BindQueryParameter("form", true, false, "kind", r.URL.Query(), ¶ms.Kind) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "kind", Err: err}) + return + } + + // ------------- Optional query parameter "name" ------------- + + err = runtime.BindQueryParameter("form", true, false, "name", r.URL.Query(), ¶ms.Name) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListSnapshots(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteSnapshot operation middleware +func (siw *ServerInterfaceWrapper) DeleteSnapshot(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "snapshotId" ------------- + var snapshotId string + + err = runtime.BindStyledParameterWithOptions("simple", "snapshotId", chi.URLParam(r, "snapshotId"), &snapshotId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "snapshotId", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteSnapshot(w, r, snapshotId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetSnapshot operation middleware +func (siw *ServerInterfaceWrapper) GetSnapshot(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "snapshotId" ------------- + var snapshotId string + + err = runtime.BindStyledParameterWithOptions("simple", "snapshotId", chi.URLParam(r, "snapshotId"), &snapshotId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "snapshotId", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetSnapshot(w, r, snapshotId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// ForkSnapshot operation middleware +func (siw *ServerInterfaceWrapper) ForkSnapshot(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "snapshotId" ------------- + var snapshotId string + + err = runtime.BindStyledParameterWithOptions("simple", "snapshotId", chi.URLParam(r, "snapshotId"), &snapshotId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "snapshotId", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ForkSnapshot(w, r, snapshotId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListVolumes operation middleware func (siw *ServerInterfaceWrapper) ListVolumes(w http.ResponseWriter, r *http.Request) { @@ -8253,6 +9625,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/instances/{id}/restore", wrapper.RestoreInstance) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/instances/{id}/snapshots", wrapper.CreateInstanceSnapshot) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/instances/{id}/snapshots/{snapshotId}/restore", wrapper.RestoreInstanceSnapshot) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/instances/{id}/standby", wrapper.StandbyInstance) }) @@ -8277,6 +9655,18 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/resources", wrapper.GetResources) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/snapshots", wrapper.ListSnapshots) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/snapshots/{snapshotId}", wrapper.DeleteSnapshot) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/snapshots/{snapshotId}", wrapper.GetSnapshot) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/snapshots/{snapshotId}/fork", wrapper.ForkSnapshot) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/volumes", wrapper.ListVolumes) }) @@ -9335,33 +10725,160 @@ func (response RestoreInstance200JSONResponse) VisitRestoreInstanceResponse(w ht return json.NewEncoder(w).Encode(response) } -type RestoreInstance404JSONResponse Error +type RestoreInstance404JSONResponse Error + +func (response RestoreInstance404JSONResponse) VisitRestoreInstanceResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type RestoreInstance409JSONResponse Error + +func (response RestoreInstance409JSONResponse) VisitRestoreInstanceResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type RestoreInstance500JSONResponse Error + +func (response RestoreInstance500JSONResponse) VisitRestoreInstanceResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateInstanceSnapshotRequestObject struct { + Id string `json:"id"` + Body *CreateInstanceSnapshotJSONRequestBody +} + +type CreateInstanceSnapshotResponseObject interface { + VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error +} + +type CreateInstanceSnapshot201JSONResponse Snapshot + +func (response CreateInstanceSnapshot201JSONResponse) VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateInstanceSnapshot400JSONResponse Error + +func (response CreateInstanceSnapshot400JSONResponse) VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateInstanceSnapshot404JSONResponse Error + +func (response CreateInstanceSnapshot404JSONResponse) VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type CreateInstanceSnapshot409JSONResponse Error + +func (response CreateInstanceSnapshot409JSONResponse) VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type CreateInstanceSnapshot500JSONResponse Error + +func (response CreateInstanceSnapshot500JSONResponse) VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateInstanceSnapshot501JSONResponse Error + +func (response CreateInstanceSnapshot501JSONResponse) VisitCreateInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(501) + + return json.NewEncoder(w).Encode(response) +} + +type RestoreInstanceSnapshotRequestObject struct { + Id string `json:"id"` + SnapshotId string `json:"snapshotId"` + Body *RestoreInstanceSnapshotJSONRequestBody +} + +type RestoreInstanceSnapshotResponseObject interface { + VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error +} + +type RestoreInstanceSnapshot200JSONResponse Instance + +func (response RestoreInstanceSnapshot200JSONResponse) VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type RestoreInstanceSnapshot400JSONResponse Error + +func (response RestoreInstanceSnapshot400JSONResponse) VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type RestoreInstanceSnapshot404JSONResponse Error -func (response RestoreInstance404JSONResponse) VisitRestoreInstanceResponse(w http.ResponseWriter) error { +func (response RestoreInstanceSnapshot404JSONResponse) VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type RestoreInstance409JSONResponse Error +type RestoreInstanceSnapshot409JSONResponse Error -func (response RestoreInstance409JSONResponse) VisitRestoreInstanceResponse(w http.ResponseWriter) error { +func (response RestoreInstanceSnapshot409JSONResponse) VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(409) return json.NewEncoder(w).Encode(response) } -type RestoreInstance500JSONResponse Error +type RestoreInstanceSnapshot500JSONResponse Error -func (response RestoreInstance500JSONResponse) VisitRestoreInstanceResponse(w http.ResponseWriter) error { +func (response RestoreInstanceSnapshot500JSONResponse) VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } +type RestoreInstanceSnapshot501JSONResponse Error + +func (response RestoreInstanceSnapshot501JSONResponse) VisitRestoreInstanceSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(501) + + return json.NewEncoder(w).Encode(response) +} + type StandbyInstanceRequestObject struct { Id string `json:"id"` } @@ -9682,6 +11199,164 @@ func (response GetResources500JSONResponse) VisitGetResourcesResponse(w http.Res return json.NewEncoder(w).Encode(response) } +type ListSnapshotsRequestObject struct { + Params ListSnapshotsParams +} + +type ListSnapshotsResponseObject interface { + VisitListSnapshotsResponse(w http.ResponseWriter) error +} + +type ListSnapshots200JSONResponse []Snapshot + +func (response ListSnapshots200JSONResponse) VisitListSnapshotsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListSnapshots500JSONResponse Error + +func (response ListSnapshots500JSONResponse) VisitListSnapshotsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteSnapshotRequestObject struct { + SnapshotId string `json:"snapshotId"` +} + +type DeleteSnapshotResponseObject interface { + VisitDeleteSnapshotResponse(w http.ResponseWriter) error +} + +type DeleteSnapshot204Response struct { +} + +func (response DeleteSnapshot204Response) VisitDeleteSnapshotResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type DeleteSnapshot404JSONResponse Error + +func (response DeleteSnapshot404JSONResponse) VisitDeleteSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteSnapshot500JSONResponse Error + +func (response DeleteSnapshot500JSONResponse) VisitDeleteSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetSnapshotRequestObject struct { + SnapshotId string `json:"snapshotId"` +} + +type GetSnapshotResponseObject interface { + VisitGetSnapshotResponse(w http.ResponseWriter) error +} + +type GetSnapshot200JSONResponse Snapshot + +func (response GetSnapshot200JSONResponse) VisitGetSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetSnapshot404JSONResponse Error + +func (response GetSnapshot404JSONResponse) VisitGetSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetSnapshot500JSONResponse Error + +func (response GetSnapshot500JSONResponse) VisitGetSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ForkSnapshotRequestObject struct { + SnapshotId string `json:"snapshotId"` + Body *ForkSnapshotJSONRequestBody +} + +type ForkSnapshotResponseObject interface { + VisitForkSnapshotResponse(w http.ResponseWriter) error +} + +type ForkSnapshot201JSONResponse Instance + +func (response ForkSnapshot201JSONResponse) VisitForkSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type ForkSnapshot400JSONResponse Error + +func (response ForkSnapshot400JSONResponse) VisitForkSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ForkSnapshot404JSONResponse Error + +func (response ForkSnapshot404JSONResponse) VisitForkSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type ForkSnapshot409JSONResponse Error + +func (response ForkSnapshot409JSONResponse) VisitForkSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type ForkSnapshot500JSONResponse Error + +func (response ForkSnapshot500JSONResponse) VisitForkSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ForkSnapshot501JSONResponse Error + +func (response ForkSnapshot501JSONResponse) VisitForkSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(501) + + return json.NewEncoder(w).Encode(response) +} + type ListVolumesRequestObject struct { } @@ -9981,6 +11656,12 @@ type StrictServerInterface interface { // Restore instance from standby // (POST /instances/{id}/restore) RestoreInstance(ctx context.Context, request RestoreInstanceRequestObject) (RestoreInstanceResponseObject, error) + // Create a snapshot for an instance + // (POST /instances/{id}/snapshots) + CreateInstanceSnapshot(ctx context.Context, request CreateInstanceSnapshotRequestObject) (CreateInstanceSnapshotResponseObject, error) + // Restore an instance from a snapshot in-place + // (POST /instances/{id}/snapshots/{snapshotId}/restore) + RestoreInstanceSnapshot(ctx context.Context, request RestoreInstanceSnapshotRequestObject) (RestoreInstanceSnapshotResponseObject, error) // Put instance in standby (pause, snapshot, delete VMM) // (POST /instances/{id}/standby) StandbyInstance(ctx context.Context, request StandbyInstanceRequestObject) (StandbyInstanceResponseObject, error) @@ -10005,6 +11686,18 @@ type StrictServerInterface interface { // Get host resource capacity and allocations // (GET /resources) GetResources(ctx context.Context, request GetResourcesRequestObject) (GetResourcesResponseObject, error) + // List snapshots + // (GET /snapshots) + ListSnapshots(ctx context.Context, request ListSnapshotsRequestObject) (ListSnapshotsResponseObject, error) + // Delete a snapshot + // (DELETE /snapshots/{snapshotId}) + DeleteSnapshot(ctx context.Context, request DeleteSnapshotRequestObject) (DeleteSnapshotResponseObject, error) + // Get snapshot details + // (GET /snapshots/{snapshotId}) + GetSnapshot(ctx context.Context, request GetSnapshotRequestObject) (GetSnapshotResponseObject, error) + // Fork a new instance from a snapshot + // (POST /snapshots/{snapshotId}/fork) + ForkSnapshot(ctx context.Context, request ForkSnapshotRequestObject) (ForkSnapshotResponseObject, error) // List volumes // (GET /volumes) ListVolumes(ctx context.Context, request ListVolumesRequestObject) (ListVolumesResponseObject, error) @@ -10749,6 +12442,73 @@ func (sh *strictHandler) RestoreInstance(w http.ResponseWriter, r *http.Request, } } +// CreateInstanceSnapshot operation middleware +func (sh *strictHandler) CreateInstanceSnapshot(w http.ResponseWriter, r *http.Request, id string) { + var request CreateInstanceSnapshotRequestObject + + request.Id = id + + var body CreateInstanceSnapshotJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateInstanceSnapshot(ctx, request.(CreateInstanceSnapshotRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateInstanceSnapshot") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateInstanceSnapshotResponseObject); ok { + if err := validResponse.VisitCreateInstanceSnapshotResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// RestoreInstanceSnapshot operation middleware +func (sh *strictHandler) RestoreInstanceSnapshot(w http.ResponseWriter, r *http.Request, id string, snapshotId string) { + var request RestoreInstanceSnapshotRequestObject + + request.Id = id + request.SnapshotId = snapshotId + + var body RestoreInstanceSnapshotJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RestoreInstanceSnapshot(ctx, request.(RestoreInstanceSnapshotRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RestoreInstanceSnapshot") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RestoreInstanceSnapshotResponseObject); ok { + if err := validResponse.VisitRestoreInstanceSnapshotResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // StandbyInstance operation middleware func (sh *strictHandler) StandbyInstance(w http.ResponseWriter, r *http.Request, id string) { var request StandbyInstanceRequestObject @@ -10972,6 +12732,117 @@ func (sh *strictHandler) GetResources(w http.ResponseWriter, r *http.Request) { } } +// ListSnapshots operation middleware +func (sh *strictHandler) ListSnapshots(w http.ResponseWriter, r *http.Request, params ListSnapshotsParams) { + var request ListSnapshotsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListSnapshots(ctx, request.(ListSnapshotsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListSnapshots") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListSnapshotsResponseObject); ok { + if err := validResponse.VisitListSnapshotsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// DeleteSnapshot operation middleware +func (sh *strictHandler) DeleteSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) { + var request DeleteSnapshotRequestObject + + request.SnapshotId = snapshotId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.DeleteSnapshot(ctx, request.(DeleteSnapshotRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "DeleteSnapshot") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(DeleteSnapshotResponseObject); ok { + if err := validResponse.VisitDeleteSnapshotResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetSnapshot operation middleware +func (sh *strictHandler) GetSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) { + var request GetSnapshotRequestObject + + request.SnapshotId = snapshotId + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetSnapshot(ctx, request.(GetSnapshotRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetSnapshot") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetSnapshotResponseObject); ok { + if err := validResponse.VisitGetSnapshotResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ForkSnapshot operation middleware +func (sh *strictHandler) ForkSnapshot(w http.ResponseWriter, r *http.Request, snapshotId string) { + var request ForkSnapshotRequestObject + + request.SnapshotId = snapshotId + + var body ForkSnapshotJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ForkSnapshot(ctx, request.(ForkSnapshotRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ForkSnapshot") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ForkSnapshotResponseObject); ok { + if err := validResponse.VisitForkSnapshotResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ListVolumes operation middleware func (sh *strictHandler) ListVolumes(w http.ResponseWriter, r *http.Request) { var request ListVolumesRequestObject @@ -11110,183 +12981,198 @@ 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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", + "Uy7VYL+/3299ef/lfwMAAP//KK+tJsQVAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/paths/paths.go b/lib/paths/paths.go index 6e4ef486..c03c0ac4 100644 --- a/lib/paths/paths.go +++ b/lib/paths/paths.go @@ -210,6 +210,26 @@ func (p *Paths) GuestsDir() string { return filepath.Join(p.dataDir, "guests") } +// SnapshotStoreDir returns the root directory for centrally managed snapshots. +func (p *Paths) SnapshotStoreDir() string { + return filepath.Join(p.dataDir, "snapshots") +} + +// SnapshotDir returns the directory for a specific snapshot. +func (p *Paths) SnapshotDir(snapshotID string) string { + return filepath.Join(p.SnapshotStoreDir(), snapshotID) +} + +// SnapshotMetadata returns the path to snapshot metadata.json. +func (p *Paths) SnapshotMetadata(snapshotID string) string { + return filepath.Join(p.SnapshotDir(snapshotID), "snapshot.json") +} + +// SnapshotGuestDir returns the path to the copied guest payload for a snapshot. +func (p *Paths) SnapshotGuestDir(snapshotID string) string { + return filepath.Join(p.SnapshotDir(snapshotID), "guest") +} + // Device path methods // DevicesDir returns the root devices directory. diff --git a/lib/snapshot/README.md b/lib/snapshot/README.md new file mode 100644 index 00000000..edcdc989 --- /dev/null +++ b/lib/snapshot/README.md @@ -0,0 +1,71 @@ +# Snapshot Feature + +Snapshots are immutable point-in-time captures of a VM that can later be: +- restored back into the original VM +- forked into a new VM +- listed, inspected, and deleted independently + +## Snapshot Kinds + +### `Standby` +- Captures standby-style state, including memory/device snapshot state plus disks. +- Intended for fast resume-style recovery. +- Can be created from `Running` or `Standby`. +- Does **not** allow hypervisor switching on restore/fork. + +### `Stopped` +- Captures disk-focused state from a stopped VM. +- Intended for cold-start style restore/fork. +- Can be created only from `Stopped`. +- Allows optional hypervisor switching on restore/fork because no memory state is carried across. + +## Lifecycle + +### Create +- `Standby` snapshot from `Running`: + - source VM is put into standby + - snapshot payload is copied + - source VM is restored to running +- `Standby` snapshot from `Standby`: + - snapshot payload is copied directly +- `Stopped` snapshot from `Stopped`: + - snapshot payload is copied directly + +### Restore (in-place) +- Restore always applies to the original source VM. +- Source VM must not be `Running`. +- Default target states: + - `Standby` snapshot -> `Running` + - `Stopped` snapshot -> `Stopped` +- Allowed target states: + - `Standby` snapshot -> `Running`, `Standby`, `Stopped` + - `Stopped` snapshot -> `Stopped`, `Running` + +### Fork (new VM) +- Creates a new instance from snapshot artifacts. +- Uses the same target-state rules as restore. +- `target_hypervisor` is allowed only for `Stopped` snapshots. + +### Delete +- Removes snapshot metadata and payload. +- Does not modify source or forked instances. + +## Safety Rules + +- Snapshot creation rejects writable volume attachments. +- Snapshot names must be unique per source instance. +- Snapshot IDs are immutable. +- Snapshot artifacts remain usable after source instance deletion. + +## Stored Data + +Each snapshot stores: +- immutable snapshot metadata (`id`, `name`, `kind`, source identity, timestamp, size) +- snapshot payload (`guest/`) used for restore/fork +- source metadata needed to reconstruct VM runtime settings during restore/fork + +## Sparse Copy Behavior + +Snapshot payload copy uses the same sparse-only guest copy path as fork. +- If sparse extent copy cannot be guaranteed, operations fail explicitly. +- No dense-copy fallback is used. diff --git a/lib/snapshot/policy.go b/lib/snapshot/policy.go new file mode 100644 index 00000000..7e29dbd0 --- /dev/null +++ b/lib/snapshot/policy.go @@ -0,0 +1,37 @@ +package snapshot + +import "fmt" + +const ( + StateStopped = "Stopped" + StateStandby = "Standby" + StateRunning = "Running" +) + +// ResolveTargetState applies snapshot defaults and validates requested target +// states for restore/fork flows. +func ResolveTargetState(kind SnapshotKind, requested string) (string, error) { + if requested == "" { + switch kind { + case SnapshotKindStandby: + return StateRunning, nil + case SnapshotKindStopped: + return StateStopped, nil + default: + return "", fmt.Errorf("unsupported snapshot kind %q", kind) + } + } + + switch kind { + case SnapshotKindStandby: + if requested == StateRunning || requested == StateStandby || requested == StateStopped { + return requested, nil + } + case SnapshotKindStopped: + if requested == StateStopped || requested == StateRunning { + return requested, nil + } + } + + return "", fmt.Errorf("invalid target_state %q for snapshot kind %s", requested, kind) +} diff --git a/lib/snapshot/record.go b/lib/snapshot/record.go new file mode 100644 index 00000000..3f06a53f --- /dev/null +++ b/lib/snapshot/record.go @@ -0,0 +1,70 @@ +package snapshot + +import ( + "encoding/json" + "fmt" +) + +// TypedRecord is a strongly-typed snapshot record for callers that want +// structured stored metadata instead of json.RawMessage. +type TypedRecord[T any] struct { + Snapshot Snapshot + StoredMetadata T +} + +func SaveTypedRecord[T any](store *Store, record *TypedRecord[T]) error { + if record == nil { + return fmt.Errorf("nil snapshot record") + } + encoded, err := encodeTypedRecord(record) + if err != nil { + return err + } + return store.SaveRecord(encoded) +} + +func LoadTypedRecord[T any](store *Store, snapshotID string) (*TypedRecord[T], error) { + record, err := store.LoadRecord(snapshotID) + if err != nil { + return nil, err + } + return decodeTypedRecord[T](record) +} + +func ListTypedRecords[T any](store *Store) ([]TypedRecord[T], error) { + records, err := store.ListRecords() + if err != nil { + return nil, err + } + typed := make([]TypedRecord[T], 0, len(records)) + for i := range records { + decoded, err := decodeTypedRecord[T](&records[i]) + if err != nil { + return nil, err + } + typed = append(typed, *decoded) + } + return typed, nil +} + +func encodeTypedRecord[T any](record *TypedRecord[T]) (*Record, error) { + metadata, err := json.Marshal(record.StoredMetadata) + if err != nil { + return nil, fmt.Errorf("marshal stored metadata: %w", err) + } + return &Record{ + Snapshot: record.Snapshot, + StoredMetadata: metadata, + }, nil +} + +func decodeTypedRecord[T any](record *Record) (*TypedRecord[T], error) { + var metadata T + if err := json.Unmarshal(record.StoredMetadata, &metadata); err != nil { + return nil, fmt.Errorf("unmarshal stored metadata for snapshot %s: %w", record.Snapshot.Id, err) + } + return &TypedRecord[T]{ + Snapshot: record.Snapshot, + StoredMetadata: metadata, + }, nil +} diff --git a/lib/snapshot/store.go b/lib/snapshot/store.go new file mode 100644 index 00000000..cc6beece --- /dev/null +++ b/lib/snapshot/store.go @@ -0,0 +1,171 @@ +package snapshot + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/kernel/hypeman/lib/paths" +) + +var ( + ErrNotFound = errors.New("snapshot not found") + ErrNameExists = errors.New("snapshot name already exists") +) + +// Record is the persisted representation of a snapshot plus source metadata. +type Record struct { + Snapshot Snapshot `json:"snapshot"` + StoredMetadata json.RawMessage `json:"stored_metadata"` +} + +// Store handles snapshot metadata persistence under paths.SnapshotStoreDir(). +type Store struct { + paths *paths.Paths +} + +func NewStore(p *paths.Paths) *Store { + return &Store{paths: p} +} + +func (s *Store) List(filter *ListSnapshotsFilter) ([]Snapshot, error) { + records, err := s.ListRecords() + if err != nil { + return nil, err + } + out := make([]Snapshot, 0, len(records)) + for _, rec := range records { + snap := rec.Snapshot + if filter == nil || filter.Matches(&snap) { + out = append(out, snap) + } + } + return out, nil +} + +func (s *Store) Get(snapshotID string) (*Snapshot, error) { + record, err := s.LoadRecord(snapshotID) + if err != nil { + return nil, err + } + out := record.Snapshot + return &out, nil +} + +func (s *Store) SaveRecord(record *Record) error { + if record == nil { + return fmt.Errorf("nil snapshot record") + } + dir := s.paths.SnapshotDir(record.Snapshot.Id) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("create snapshot directory: %w", err) + } + content, err := json.MarshalIndent(record, "", " ") + if err != nil { + return fmt.Errorf("marshal snapshot metadata: %w", err) + } + if err := os.WriteFile(s.paths.SnapshotMetadata(record.Snapshot.Id), content, 0644); err != nil { + return fmt.Errorf("write snapshot metadata: %w", err) + } + return nil +} + +func (s *Store) LoadRecord(snapshotID string) (*Record, error) { + content, err := os.ReadFile(s.paths.SnapshotMetadata(snapshotID)) + if err != nil { + if os.IsNotExist(err) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("read snapshot metadata: %w", err) + } + var record Record + if err := json.Unmarshal(content, &record); err != nil { + return nil, fmt.Errorf("unmarshal snapshot metadata: %w", err) + } + if record.Snapshot.Id == "" { + record.Snapshot.Id = snapshotID + } + return &record, nil +} + +func (s *Store) ListRecords() ([]Record, error) { + if err := os.MkdirAll(s.paths.SnapshotStoreDir(), 0755); err != nil { + return nil, fmt.Errorf("create snapshot store directory: %w", err) + } + entries, err := os.ReadDir(s.paths.SnapshotStoreDir()) + if err != nil { + return nil, fmt.Errorf("read snapshot store directory: %w", err) + } + records := make([]Record, 0, len(entries)) + for _, entry := range entries { + if !entry.IsDir() { + continue + } + record, err := s.LoadRecord(entry.Name()) + if err != nil { + if errors.Is(err, ErrNotFound) { + continue + } + return nil, err + } + records = append(records, *record) + } + return records, nil +} + +func (s *Store) Delete(snapshotID string) error { + path := s.paths.SnapshotDir(snapshotID) + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return ErrNotFound + } + return fmt.Errorf("stat snapshot directory: %w", err) + } + if err := os.RemoveAll(path); err != nil { + return fmt.Errorf("delete snapshot: %w", err) + } + return nil +} + +func (s *Store) EnsureNameAvailable(sourceInstanceID, snapshotName string) error { + if snapshotName == "" { + return nil + } + records, err := s.ListRecords() + if err != nil { + return err + } + for _, record := range records { + if record.Snapshot.SourceInstanceID == sourceInstanceID && record.Snapshot.Name == snapshotName { + return fmt.Errorf("%w: snapshot name %q already exists for source instance %s", ErrNameExists, snapshotName, sourceInstanceID) + } + } + return nil +} + +func DirectoryFileSize(root string) (int64, error) { + var total int64 + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + return nil + } + info, err := d.Info() + if err != nil { + return err + } + if info.Mode().IsRegular() { + total += info.Size() + } + return nil + }) + if err != nil { + return 0, fmt.Errorf("walk snapshot payload: %w", err) + } + return total, nil +} diff --git a/lib/snapshot/store_test.go b/lib/snapshot/store_test.go new file mode 100644 index 00000000..e2ae3eec --- /dev/null +++ b/lib/snapshot/store_test.go @@ -0,0 +1,132 @@ +package snapshot + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/require" +) + +func TestStoreSaveLoadListDelete(t *testing.T) { + t.Parallel() + + p := paths.New(t.TempDir()) + store := NewStore(p) + + record := &Record{ + Snapshot: Snapshot{ + Id: "snap1", + Name: "baseline", + Kind: SnapshotKindStandby, + SourceInstanceID: "inst1", + SourceName: "vm1", + SourceHypervisor: hypervisor.TypeQEMU, + CreatedAt: time.Now().UTC().Truncate(time.Second), + SizeBytes: 1234, + }, + StoredMetadata: json.RawMessage(`{"id":"inst1","name":"vm1"}`), + } + + require.NoError(t, store.SaveRecord(record)) + + got, err := store.LoadRecord(record.Snapshot.Id) + require.NoError(t, err) + require.Equal(t, record.Snapshot.Id, got.Snapshot.Id) + require.JSONEq(t, string(record.StoredMetadata), string(got.StoredMetadata)) + + listed, err := store.List(nil) + require.NoError(t, err) + require.Len(t, listed, 1) + require.Equal(t, record.Snapshot.Id, listed[0].Id) + + require.NoError(t, store.Delete(record.Snapshot.Id)) + _, err = store.LoadRecord(record.Snapshot.Id) + require.Error(t, err) + require.True(t, errors.Is(err, ErrNotFound)) +} + +func TestStoreEnsureNameAvailable(t *testing.T) { + t.Parallel() + + p := paths.New(t.TempDir()) + store := NewStore(p) + + require.NoError(t, store.SaveRecord(&Record{ + Snapshot: Snapshot{ + Id: "snap1", + Name: "baseline", + Kind: SnapshotKindStandby, + SourceInstanceID: "inst1", + }, + StoredMetadata: json.RawMessage(`{}`), + })) + + require.NoError(t, store.EnsureNameAvailable("inst1", "different")) + require.NoError(t, store.EnsureNameAvailable("inst2", "baseline")) + + err := store.EnsureNameAvailable("inst1", "baseline") + require.Error(t, err) + require.True(t, errors.Is(err, ErrNameExists)) +} + +func TestListSnapshotsFilterMatches(t *testing.T) { + t.Parallel() + + kind := SnapshotKindStandby + sourceID := "inst1" + name := "snap" + filter := &ListSnapshotsFilter{ + SourceInstanceID: &sourceID, + Kind: &kind, + Name: &name, + } + + require.True(t, filter.Matches(&Snapshot{ + SourceInstanceID: "inst1", + Kind: SnapshotKindStandby, + Name: "snap", + })) + require.False(t, filter.Matches(&Snapshot{ + SourceInstanceID: "inst2", + Kind: SnapshotKindStandby, + Name: "snap", + })) +} + +func TestDirectoryFileSize(t *testing.T) { + t.Parallel() + + root := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(root, "a.bin"), []byte("abc"), 0644)) + require.NoError(t, os.MkdirAll(filepath.Join(root, "nested"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(root, "nested", "b.bin"), []byte("12345"), 0644)) + + size, err := DirectoryFileSize(root) + require.NoError(t, err) + require.Equal(t, int64(8), size) +} + +func TestResolveTargetState(t *testing.T) { + t.Parallel() + + state, err := ResolveTargetState(SnapshotKindStandby, "") + require.NoError(t, err) + require.Equal(t, StateRunning, state) + + state, err = ResolveTargetState(SnapshotKindStopped, "") + require.NoError(t, err) + require.Equal(t, StateStopped, state) + + state, err = ResolveTargetState(SnapshotKindStandby, StateStandby) + require.NoError(t, err) + require.Equal(t, StateStandby, state) + + _, err = ResolveTargetState(SnapshotKindStopped, StateStandby) + require.Error(t, err) +} diff --git a/lib/snapshot/testsupport/images.go b/lib/snapshot/testsupport/images.go new file mode 100644 index 00000000..490bfea1 --- /dev/null +++ b/lib/snapshot/testsupport/images.go @@ -0,0 +1,147 @@ +package testsupport + +import ( + "context" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/kernel/hypeman/lib/images" + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/require" +) + +// EnsureImageReady pre-warms a shared image cache under /tmp and seeds that +// image into the test data directory so instance integration tests don't need +// to repull/reconvert from scratch. +func EnsureImageReady(t *testing.T, ctx context.Context, p *paths.Paths, imageManager images.Manager, image string) { + t.Helper() + + ref, err := images.ParseNormalizedRef(image) + require.NoError(t, err) + + cachePaths := paths.New(filepath.Join(os.TempDir(), "hypeman-snapshot-image-cache")) + cacheMgr, err := images.NewManager(cachePaths, 1, nil) + require.NoError(t, err) + + prewarmCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + created, err := cacheMgr.CreateImage(prewarmCtx, images.CreateImageRequest{Name: image}) + require.NoError(t, err) + + waitName := created.Name + if created.Digest != "" { + waitName = fmt.Sprintf("%s@%s", ref.Repository(), created.Digest) + } + require.NoError(t, cacheMgr.WaitForReady(prewarmCtx, waitName)) + + cached, err := cacheMgr.GetImage(prewarmCtx, waitName) + require.NoError(t, err) + require.Equal(t, images.StatusReady, cached.Status) + require.NotEmpty(t, cached.Digest) + + digestHex := strings.TrimPrefix(cached.Digest, "sha256:") + require.NotEmpty(t, digestHex) + + srcDigestDir := cachePaths.ImageDigestDir(ref.Repository(), digestHex) + dstDigestDir := p.ImageDigestDir(ref.Repository(), digestHex) + require.NoError(t, copyDirWithHardlinks(srcDigestDir, dstDigestDir)) + + if ref.Tag() != "" { + linkPath := p.ImageTagSymlink(ref.Repository(), ref.Tag()) + require.NoError(t, os.MkdirAll(filepath.Dir(linkPath), 0755)) + _ = os.Remove(linkPath) + require.NoError(t, os.Symlink(digestHex, linkPath)) + } + + reference := ref.Tag() + if reference == "" { + reference = cached.Digest + } + imported, err := imageManager.ImportLocalImage(ctx, ref.Repository(), reference, cached.Digest) + require.NoError(t, err) + + if imported.Status != images.StatusReady { + waitCtx, waitCancel := context.WithTimeout(ctx, 2*time.Minute) + defer waitCancel() + require.NoError(t, imageManager.WaitForReady(waitCtx, imported.Name)) + } +} + +func copyDirWithHardlinks(srcDir, dstDir string) error { + srcInfo, err := os.Stat(srcDir) + if err != nil { + return err + } + if !srcInfo.IsDir() { + return fmt.Errorf("source is not a directory: %s", srcDir) + } + if err := os.MkdirAll(dstDir, srcInfo.Mode().Perm()); err != nil { + return err + } + + return filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + rel, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + + dstPath := filepath.Join(dstDir, rel) + info, err := d.Info() + if err != nil { + return err + } + + if info.Mode().IsDir() { + return os.MkdirAll(dstPath, info.Mode().Perm()) + } + if info.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(path) + if err != nil { + return err + } + _ = os.Remove(dstPath) + return os.Symlink(target, dstPath) + } + if !info.Mode().IsRegular() { + return nil + } + + _ = os.Remove(dstPath) + if err := os.Link(path, dstPath); err == nil { + return nil + } + return copyRegularFile(path, dstPath, info.Mode().Perm()) + }) +} + +func copyRegularFile(src, dst string, perms fs.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perms) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Sync() +} diff --git a/lib/snapshot/types.go b/lib/snapshot/types.go new file mode 100644 index 00000000..38ed6619 --- /dev/null +++ b/lib/snapshot/types.go @@ -0,0 +1,53 @@ +package snapshot + +import ( + "time" + + "github.com/kernel/hypeman/lib/hypervisor" +) + +// SnapshotKind determines how snapshot data is captured and restored. +type SnapshotKind string + +const ( + // SnapshotKindStandby captures snapshot-based standby state (memory/device/disk). + SnapshotKindStandby SnapshotKind = "Standby" + // SnapshotKindStopped captures stopped-state disk+metadata only. + SnapshotKindStopped SnapshotKind = "Stopped" +) + +// Snapshot is a centrally stored immutable snapshot resource. +type Snapshot struct { + Id string `json:"id"` + Name string `json:"name"` + Kind SnapshotKind `json:"kind"` + SourceInstanceID string `json:"source_instance_id"` + SourceName string `json:"source_instance_name"` + SourceHypervisor hypervisor.Type + CreatedAt time.Time `json:"created_at"` + SizeBytes int64 `json:"size_bytes"` +} + +// ListSnapshotsFilter contains optional filters for listing snapshots. +type ListSnapshotsFilter struct { + SourceInstanceID *string + Kind *SnapshotKind + Name *string +} + +// Matches returns true if the given snapshot satisfies all filter criteria. +func (f *ListSnapshotsFilter) Matches(snapshot *Snapshot) bool { + if f == nil { + return true + } + if f.SourceInstanceID != nil && snapshot.SourceInstanceID != *f.SourceInstanceID { + return false + } + if f.Kind != nil && snapshot.Kind != *f.Kind { + return false + } + if f.Name != nil && snapshot.Name != *f.Name { + return false + } + return true +} diff --git a/openapi.yaml b/openapi.yaml index 080499c2..7d09d015 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -246,6 +246,113 @@ components: description: Target state for the forked instance after fork completes enum: [Stopped, Standby, Running] example: Running + + SnapshotKind: + type: string + description: Snapshot capture kind + enum: [Standby, Stopped] + example: Standby + + SnapshotTargetState: + type: string + description: Target state when restoring or forking from a snapshot + enum: [Stopped, Standby, Running] + example: Running + + Snapshot: + type: object + required: [id, kind, source_instance_id, source_instance_name, source_hypervisor, created_at, size_bytes] + properties: + id: + type: string + description: Auto-generated unique snapshot identifier + example: q7z1w7l2af4l8y7q1h7g2m3s + name: + type: string + description: Optional human-readable snapshot name (unique per source instance) + nullable: true + example: baseline-standby + kind: + $ref: "#/components/schemas/SnapshotKind" + source_instance_id: + type: string + description: Source instance ID at snapshot creation time + example: qilviffnqzck2jrim1x6s2b1 + source_instance_name: + type: string + description: Source instance name at snapshot creation time + example: nginx1 + source_hypervisor: + type: string + enum: [cloud-hypervisor, firecracker, qemu, vz] + description: Source instance hypervisor at snapshot creation time + example: cloud-hypervisor + created_at: + type: string + format: date-time + description: Snapshot creation timestamp + example: "2026-03-06T13:56:11Z" + size_bytes: + type: integer + format: int64 + description: Total payload size in bytes + example: 104857600 + + CreateSnapshotRequest: + type: object + required: [kind] + properties: + kind: + $ref: "#/components/schemas/SnapshotKind" + name: + type: string + description: Optional snapshot name (lowercase letters, digits, and dashes only; cannot start or end with a dash) + pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ + maxLength: 63 + example: pre-upgrade + + RestoreSnapshotRequest: + type: object + properties: + target_state: + $ref: "#/components/schemas/SnapshotTargetState" + description: | + Optional final state after restore. + Defaults by snapshot kind: + - Standby snapshot defaults to Running + - Stopped snapshot defaults to Stopped + target_hypervisor: + type: string + enum: [cloud-hypervisor, firecracker, qemu, vz] + description: | + Optional hypervisor override. Allowed only when restoring from a Stopped snapshot. + Standby snapshots must restore with their original hypervisor. + example: qemu + + ForkSnapshotRequest: + type: object + required: [name] + properties: + name: + type: string + description: Name for the new instance (lowercase letters, digits, and dashes only; cannot start or end with a dash) + pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ + maxLength: 63 + example: nginx-from-snap + target_state: + $ref: "#/components/schemas/SnapshotTargetState" + description: | + Optional final state for the forked instance. + Defaults by snapshot kind: + - Standby snapshot defaults to Running + - Stopped snapshot defaults to Stopped + target_hypervisor: + type: string + enum: [cloud-hypervisor, firecracker, qemu, vz] + description: | + Optional hypervisor override. Allowed only when forking from a Stopped snapshot. + Standby snapshots must fork with their original hypervisor. + example: cloud-hypervisor Instance: type: object @@ -1601,6 +1708,284 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + + /instances/{id}/snapshots: + post: + summary: Create a snapshot for an instance + operationId: createInstanceSnapshot + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Source instance ID or name + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSnapshotRequest" + responses: + 201: + description: Snapshot created + content: + application/json: + schema: + $ref: "#/components/schemas/Snapshot" + 400: + description: Bad request - invalid snapshot request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 404: + description: Source instance not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 409: + description: Conflict - invalid state or duplicate snapshot name + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 501: + description: Not implemented - operation unsupported by source hypervisor + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /instances/{id}/snapshots/{snapshotId}/restore: + post: + summary: Restore an instance from a snapshot in-place + operationId: restoreInstanceSnapshot + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + description: Source instance ID or name + - name: snapshotId + in: path + required: true + schema: + type: string + description: Snapshot ID + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RestoreSnapshotRequest" + responses: + 200: + description: Instance restored from snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + 400: + description: Bad request - invalid restore request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 404: + description: Instance or snapshot not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 409: + description: Conflict - invalid source state or unsupported transition + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 501: + description: Not implemented - operation unsupported by target hypervisor + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /snapshots: + get: + summary: List snapshots + operationId: listSnapshots + security: + - bearerAuth: [] + parameters: + - name: source_instance_id + in: query + required: false + schema: + type: string + description: Filter snapshots by source instance ID + - name: kind + in: query + required: false + schema: + $ref: "#/components/schemas/SnapshotKind" + description: Filter snapshots by kind + - name: name + in: query + required: false + schema: + type: string + description: Filter snapshots by snapshot name + responses: + 200: + description: List of snapshots + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Snapshot" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /snapshots/{snapshotId}: + get: + summary: Get snapshot details + operationId: getSnapshot + security: + - bearerAuth: [] + parameters: + - name: snapshotId + in: path + required: true + schema: + type: string + description: Snapshot ID + responses: + 200: + description: Snapshot details + content: + application/json: + schema: + $ref: "#/components/schemas/Snapshot" + 404: + description: Snapshot not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + delete: + summary: Delete a snapshot + operationId: deleteSnapshot + security: + - bearerAuth: [] + parameters: + - name: snapshotId + in: path + required: true + schema: + type: string + description: Snapshot ID + responses: + 204: + description: Snapshot deleted + 404: + description: Snapshot not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /snapshots/{snapshotId}/fork: + post: + summary: Fork a new instance from a snapshot + operationId: forkSnapshot + security: + - bearerAuth: [] + parameters: + - name: snapshotId + in: path + required: true + schema: + type: string + description: Snapshot ID + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ForkSnapshotRequest" + responses: + 201: + description: Forked instance created from snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/Instance" + 400: + description: Bad request - invalid fork snapshot request + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 404: + description: Snapshot not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 409: + description: Conflict - invalid target state or name conflict + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 501: + description: Not implemented - operation unsupported by target hypervisor + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 500: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /instances/{id}/stop: post: diff --git a/stainless.yaml b/stainless.yaml index 513a5baa..53f516fd 100644 --- a/stainless.yaml +++ b/stainless.yaml @@ -92,6 +92,20 @@ resources: methods: attach: post /instances/{id}/volumes/{volumeId} detach: delete /instances/{id}/volumes/{volumeId} + snapshots: + methods: + create: post /instances/{id}/snapshots + restore: post /instances/{id}/snapshots/{snapshotId}/restore + + snapshots: + models: + snapshot: "#/components/schemas/Snapshot" + snapshot_kind: "#/components/schemas/SnapshotKind" + methods: + list: get /snapshots + get: get /snapshots/{snapshotId} + delete: delete /snapshots/{snapshotId} + fork: post /snapshots/{snapshotId}/fork volumes: models: