repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/runstate/runstate.go
Rawpackage runstate
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
type Active struct {
RunID string `json:"run_id"`
ChildID string `json:"child_id"`
Repo string `json:"repo"`
Job string `json:"job"`
Rev string `json:"rev"`
Ref string `json:"ref,omitempty"`
Trigger string `json:"trigger"`
StartedAt time.Time `json:"started_at"`
}
type Store struct{ DataDir string }
func (s Store) Write(active Active) error {
if err := active.validate(active.RunID); err != nil {
return err
}
dir := filepath.Join(s.DataDir, "active")
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
active.StartedAt = active.StartedAt.UTC()
data, err := json.Marshal(active)
if err != nil {
return err
}
data = append(data, '\n')
path := filepath.Join(dir, active.RunID+".json")
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp)
return err
}
return nil
}
func (s Store) Remove(runID string) error {
if err := cleanID(runID); err != nil {
return err
}
return os.Remove(filepath.Join(s.DataDir, "active", runID+".json"))
}
func (s Store) List() ([]Active, []error, error) {
dir := filepath.Join(s.DataDir, "active")
entries, err := os.ReadDir(dir)
if err != nil {
return nil, nil, err
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })
var out []Active
var warnings []error
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
warnings = append(warnings, fmt.Errorf("%s: %w", entry.Name(), err))
continue
}
var active Active
if err := json.Unmarshal(data, &active); err != nil {
warnings = append(warnings, fmt.Errorf("%s: %w", entry.Name(), err))
continue
}
stem := strings.TrimSuffix(entry.Name(), ".json")
if err := active.validate(stem); err != nil {
warnings = append(warnings, fmt.Errorf("%s: %w", entry.Name(), err))
continue
}
out = append(out, active)
}
return out, warnings, nil
}
func cleanID(id string) error {
if id == "" || filepath.Base(id) != id {
return fmt.Errorf("unsafe run id %q", id)
}
return nil
}
func (a Active) validate(stem string) error {
if err := cleanID(a.RunID); err != nil {
return err
}
if a.RunID != stem {
return fmt.Errorf("run id %q does not match filename", a.RunID)
}
ordinal, err := strconv.Atoi(a.ChildID)
if err != nil || ordinal < 1 {
return fmt.Errorf("invalid child id %q", a.ChildID)
}
if strings.TrimSpace(a.Repo) == "" || strings.TrimSpace(a.Job) == "" || strings.TrimSpace(a.Rev) == "" {
return fmt.Errorf("active record requires repo, job, and revision")
}
if a.Trigger != "push" && a.Trigger != "manual" && a.Trigger != "schedule" {
return fmt.Errorf("invalid trigger %q", a.Trigger)
}
if a.StartedAt.IsZero() {
return fmt.Errorf("active record requires start time")
}
return nil
}