package store import ( "database/sql" "errors" "time" ) var ErrInstanceLimit = errors.New("instance limit reached") var ErrIntentState = errors.New("intent is no longer waiting") type Intent struct { ID string `json:"id"` UID string `json:"uid"` Name string `json:"name"` Version string `json:"version"` BuildID string `json:"build_id"` InstanceID string `json:"instance_id"` Status string `json:"status"` Error string `json:"error"` Host string `json:"-"` Created int64 `json:"created_at"` } func (s *Store) EnsureIntents() error { _, err := s.DB.Exec(`CREATE TABLE IF NOT EXISTS creation_intents(id TEXT PRIMARY KEY,uid TEXT NOT NULL,name TEXT NOT NULL,version TEXT NOT NULL,build_id TEXT NOT NULL DEFAULT '',instance_id TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,error TEXT NOT NULL DEFAULT '',host TEXT NOT NULL,created INTEGER NOT NULL); CREATE INDEX IF NOT EXISTS intents_owner ON creation_intents(uid,status);`) return err } func reserveSlot(tx *sql.Tx, uid string) error { var count int if err := tx.QueryRow(`SELECT (SELECT count(*) FROM instances WHERE uid=?) + (SELECT count(*) FROM creation_intents WHERE uid=? AND status='waiting')`, uid, uid).Scan(&count); err != nil { return err } if count >= 2 { return ErrInstanceLimit } return nil } func (s *Store) ReserveIntent(i Intent) error { tx, err := s.DB.Begin() if err != nil { return err } defer tx.Rollback() if err = reserveSlot(tx, i.UID); err != nil { return err } _, err = tx.Exec(`INSERT INTO creation_intents(id,uid,name,version,status,host,created) VALUES(?,?,?,?,'waiting',?,?)`, i.ID, i.UID, i.Name, i.Version, i.Host, time.Now().Unix()) if err != nil { return err } return tx.Commit() } func (s *Store) ReserveInstance(i Instance) error { tx, err := s.DB.Begin() if err != nil { return err } defer tx.Rollback() if err = reserveSlot(tx, i.UID); err != nil { return err } if err = insertInstance(tx, i); err != nil { return err } return tx.Commit() } func insertInstance(tx *sql.Tx, i Instance) error { _, err := tx.Exec(`INSERT INTO instances VALUES(?,?,?,?,?,?,?,?)`, i.ID, i.UID, i.Name, i.Version, i.ImageID, i.Status, i.ContainerID, time.Now().Unix()) return err } func (s *Store) ClaimIntent(id string, i Instance) error { tx, err := s.DB.Begin() if err != nil { return err } defer tx.Rollback() result, err := tx.Exec(`UPDATE creation_intents SET status='creating',instance_id=? WHERE id=? AND uid=? AND version=? AND status='waiting'`, i.ID, id, i.UID, i.Version) if err != nil { return err } n, err := result.RowsAffected() if err != nil { return err } if n != 1 { return ErrIntentState } if err = insertInstance(tx, i); err != nil { return err } return tx.Commit() } func (s *Store) CancelIntent(id, uid string) error { r, err := s.DB.Exec(`UPDATE creation_intents SET status='cancelled' WHERE id=? AND uid=? AND status='waiting'`, id, uid) if err != nil { return err } n, err := r.RowsAffected() if err != nil { return err } if n != 1 { return ErrIntentState } return nil } func (s *Store) Intents(uid string) ([]Intent, error) { query := `SELECT id,uid,name,version,build_id,instance_id,status,error,host,created FROM creation_intents` var args []any if uid != "" { query += ` WHERE uid=?` args = append(args, uid) } query += ` ORDER BY created,id` rows, err := s.DB.Query(query, args...) if err != nil { return nil, err } defer rows.Close() out := []Intent{} for rows.Next() { var i Intent if err = rows.Scan(&i.ID, &i.UID, &i.Name, &i.Version, &i.BuildID, &i.InstanceID, &i.Status, &i.Error, &i.Host, &i.Created); err != nil { return nil, err } out = append(out, i) } return out, rows.Err() }