Skip to content

Commit f19854a

Browse files
committed
feat: delete instance
1 parent 03e0e5c commit f19854a

File tree

17 files changed

+250
-10
lines changed

17 files changed

+250
-10
lines changed

docs/images/dashboard-cuted.png

-1.69 KB
Loading

docs/images/dashboard.png

3.52 KB
Loading

internal/adapters/http/handlers.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,31 @@ func (h *Handlers) AdminRefreshStars(w http.ResponseWriter, r *http.Request) {
432432
w.WriteHeader(http.StatusOK)
433433
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": "Stars refreshed"})
434434
}
435+
436+
// AdminDeleteInstance handles instance deletion requests.
437+
func (h *Handlers) AdminDeleteInstance(w http.ResponseWriter, r *http.Request) {
438+
if r.Method != http.MethodDelete {
439+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
440+
return
441+
}
442+
443+
// Extract instance_id from path /api/v1/admin/instances/{instance_id}
444+
instanceID := r.URL.Path[len("/api/v1/admin/instances/"):]
445+
if instanceID == "" {
446+
http.Error(w, "Instance ID required", http.StatusBadRequest)
447+
return
448+
}
449+
450+
h.logger.Info("deleting instance", "instance_id", instanceID)
451+
452+
err := h.instances.Delete(r.Context(), instanceID)
453+
if err != nil {
454+
h.logger.Error("failed to delete instance", "instance_id", instanceID, "error", err)
455+
http.Error(w, err.Error(), http.StatusBadRequest)
456+
return
457+
}
458+
459+
h.logger.Info("instance deleted", "instance_id", instanceID)
460+
w.WriteHeader(http.StatusOK)
461+
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok", "message": "Instance deleted"})
462+
}

internal/adapters/http/handlers_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ func (m *mockInstanceRepo) UpdateStatus(ctx context.Context, id domain.InstanceI
7171
return nil
7272
}
7373

74+
func (m *mockInstanceRepo) Delete(ctx context.Context, id domain.InstanceID) error {
75+
if _, ok := m.instances[id.String()]; !ok {
76+
return domain.ErrInstanceNotFound
77+
}
78+
delete(m.instances, id.String())
79+
return nil
80+
}
81+
7482
type mockSnapshotRepo struct {
7583
snapshots []*domain.Snapshot
7684
saveErr error

internal/adapters/http/router.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ func NewRouter(cfg RouterConfig) *http.ServeMux {
7474
mux.HandleFunc("/v1/activate", rl.RegisterMiddleware(authMW.RequireSignature(handlers.Activate)))
7575
mux.HandleFunc("/v1/snapshot", rl.SnapshotMiddleware(authMW.RequireSignature(handlers.Snapshot)))
7676
mux.HandleFunc("/api/v1/admin/stats", rl.AdminMiddleware(handlers.AdminStats))
77+
mux.HandleFunc("/api/v1/admin/instances/", rl.AdminMiddleware(func(w http.ResponseWriter, r *http.Request) {
78+
if r.URL.Path == "/api/v1/admin/instances/" || r.URL.Path == "/api/v1/admin/instances" {
79+
handlers.AdminInstances(w, r)
80+
return
81+
}
82+
if r.Method == http.MethodDelete {
83+
handlers.AdminDeleteInstance(w, r)
84+
} else {
85+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
86+
}
87+
}))
7788
mux.HandleFunc("/api/v1/admin/instances", rl.AdminMiddleware(handlers.AdminInstances))
7889
mux.HandleFunc("/api/v1/admin/metrics/", rl.AdminMiddleware(handlers.AdminMetrics))
7990
mux.HandleFunc("/api/v1/admin/applications", rl.AdminMiddleware(handlers.AdminListApplications))
@@ -100,6 +111,17 @@ func NewRouter(cfg RouterConfig) *http.ServeMux {
100111
mux.HandleFunc("/v1/activate", authMW.RequireSignature(handlers.Activate))
101112
mux.HandleFunc("/v1/snapshot", authMW.RequireSignature(handlers.Snapshot))
102113
mux.HandleFunc("/api/v1/admin/stats", handlers.AdminStats)
114+
mux.HandleFunc("/api/v1/admin/instances/", func(w http.ResponseWriter, r *http.Request) {
115+
if r.URL.Path == "/api/v1/admin/instances/" || r.URL.Path == "/api/v1/admin/instances" {
116+
handlers.AdminInstances(w, r)
117+
return
118+
}
119+
if r.Method == http.MethodDelete {
120+
handlers.AdminDeleteInstance(w, r)
121+
} else {
122+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
123+
}
124+
})
103125
mux.HandleFunc("/api/v1/admin/instances", handlers.AdminInstances)
104126
mux.HandleFunc("/api/v1/admin/metrics/", handlers.AdminMetrics)
105127
mux.HandleFunc("/api/v1/admin/applications", handlers.AdminListApplications)

internal/adapters/postgres/instance.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,20 @@ func (r *InstanceRepository) UpdateStatus(ctx context.Context, id domain.Instanc
142142

143143
return nil
144144
}
145+
146+
// Delete removes an instance and all its associated snapshots.
147+
func (r *InstanceRepository) Delete(ctx context.Context, id domain.InstanceID) error {
148+
// Snapshots are deleted via ON DELETE CASCADE constraint
149+
query := `DELETE FROM instances WHERE instance_id = $1`
150+
result, err := r.db.ExecContext(ctx, query, id.String())
151+
if err != nil {
152+
return fmt.Errorf("delete instance %s: %w", id, err)
153+
}
154+
155+
rows, _ := result.RowsAffected()
156+
if rows == 0 {
157+
return domain.ErrInstanceNotFound
158+
}
159+
160+
return nil
161+
}

internal/app/instance.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,17 @@ func (s *InstanceService) Revoke(ctx context.Context, instanceID string) error {
145145

146146
return nil
147147
}
148+
149+
// Delete permanently removes an instance and all its associated snapshots.
150+
func (s *InstanceService) Delete(ctx context.Context, instanceID string) error {
151+
id, err := domain.NewInstanceID(instanceID)
152+
if err != nil {
153+
return fmt.Errorf("delete instance: %w", err)
154+
}
155+
156+
if err := s.repo.Delete(ctx, id); err != nil {
157+
return fmt.Errorf("delete instance: %w", err)
158+
}
159+
160+
return nil
161+
}

internal/app/instance_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ func (m *mockInstanceRepo) UpdateStatus(ctx context.Context, id domain.InstanceI
7272
return nil
7373
}
7474

75+
func (m *mockInstanceRepo) Delete(ctx context.Context, id domain.InstanceID) error {
76+
if _, ok := m.instances[id.String()]; !ok {
77+
return domain.ErrInstanceNotFound
78+
}
79+
delete(m.instances, id.String())
80+
return nil
81+
}
82+
7583
const (
7684
validUUID = "550e8400-e29b-41d4-a716-446655440000"
7785
validKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

internal/app/ports/repository.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ type InstanceRepository interface {
2828

2929
// UpdateStatus updates the status and last_seen_at timestamp.
3030
UpdateStatus(ctx context.Context, id domain.InstanceID, status domain.InstanceStatus) error
31+
32+
// Delete removes an instance and all its associated snapshots.
33+
// Returns domain.ErrInstanceNotFound if the instance doesn't exist.
34+
Delete(ctx context.Context, id domain.InstanceID) error
3135
}
3236

3337
// SnapshotRepository defines persistence operations for snapshots.

sdk/golang/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ client, _ := shm.New(shm.Config{
9090
9191
## How It Works
9292

93-
1. **Identity Generation**: On first run, the SDK generates an Ed25519 keypair and a unique instance ID, stored in `{DataDir}/{app-name}_shm_identity.json`
93+
1. **Identity Generation**: On first run, the SDK generates an Ed25519 keypair and a unique instance ID, stored in `{DataDir}/shm_identity.json`
9494

9595
2. **Registration**: The client registers with the server, sending its public key
9696

0 commit comments

Comments
 (0)