Persist agent/secrets settings with masked secrets and runner hot-reload; add maintenance windows, maintpage binary, and zero-downtime listener handoff.
125 lines
2.7 KiB
Go
125 lines
2.7 KiB
Go
package maintenance
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type memStore struct {
|
|
mu sync.Mutex
|
|
next int64
|
|
rows map[int64]Window
|
|
}
|
|
|
|
func newMem() *memStore {
|
|
return &memStore{rows: map[int64]Window{}, next: 1}
|
|
}
|
|
|
|
func (m *memStore) GetOpen(_ context.Context) (*Window, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, w := range m.rows {
|
|
if w.Status == StatusScheduled || w.Status == StatusActive {
|
|
cp := w
|
|
return &cp, nil
|
|
}
|
|
}
|
|
return nil, ErrNotFound
|
|
}
|
|
|
|
func (m *memStore) GetByID(_ context.Context, id int64) (*Window, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
w, ok := m.rows[id]
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
cp := w
|
|
return &cp, nil
|
|
}
|
|
|
|
func (m *memStore) Insert(_ context.Context, in Window) (Window, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, w := range m.rows {
|
|
if w.Status == StatusScheduled || w.Status == StatusActive {
|
|
return Window{}, ErrConflictOpen
|
|
}
|
|
}
|
|
in.ID = m.next
|
|
m.next++
|
|
in.Status = StatusScheduled
|
|
in.CreatedAt = time.Now()
|
|
in.UpdatedAt = in.CreatedAt
|
|
m.rows[in.ID] = in
|
|
return in, nil
|
|
}
|
|
|
|
func (m *memStore) UpdateStatus(_ context.Context, id int64, from, to string) (Window, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
w, ok := m.rows[id]
|
|
if !ok || w.Status != from {
|
|
return Window{}, ErrBadStatus
|
|
}
|
|
w.Status = to
|
|
w.UpdatedAt = time.Now()
|
|
now := w.UpdatedAt
|
|
if to == StatusActive {
|
|
w.ActivatedAt = &now
|
|
}
|
|
if to == StatusEnded || to == StatusCancelled {
|
|
w.EndedAt = &now
|
|
}
|
|
m.rows[id] = w
|
|
return w, nil
|
|
}
|
|
|
|
func (m *memStore) ListRecent(_ context.Context, limit int) ([]Window, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := make([]Window, 0, len(m.rows))
|
|
for _, w := range m.rows {
|
|
out = append(out, w)
|
|
}
|
|
if limit > 0 && len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func TestScheduleAndStatus(t *testing.T) {
|
|
svc := NewService(newMem())
|
|
start := time.Now().Add(2 * time.Hour)
|
|
end := start.Add(3 * time.Hour)
|
|
w, err := svc.Schedule(context.Background(), ScheduleInput{
|
|
Start: start, End: end, Message: "升级", CreatedBy: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
view, err := svc.CurrentStatus(context.Background())
|
|
if err != nil || !view.Scheduled || view.WindowID != w.ID {
|
|
t.Fatalf("view=%+v err=%v", view, err)
|
|
}
|
|
if _, err := svc.Cancel(context.Background(), w.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
view, _ = svc.CurrentStatus(context.Background())
|
|
if view.Scheduled || view.Active {
|
|
t.Fatalf("expected cleared, got %+v", view)
|
|
}
|
|
}
|
|
|
|
func TestInvalidWindow(t *testing.T) {
|
|
svc := NewService(newMem())
|
|
_, err := svc.Schedule(context.Background(), ScheduleInput{
|
|
Start: time.Now().Add(time.Hour), End: time.Now(), Message: "x",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|