128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
package deploy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/iceBear67/simplepages/internal/site"
|
|
"github.com/iceBear67/simplepages/internal/store"
|
|
)
|
|
|
|
// index reads a deployment's manifest and turns it into the lookup structures
|
|
// the serving path needs. This is the expensive, failure-prone half of building
|
|
// a snapshot, which is why it is separable: an activation runs it before it
|
|
// changes anything.
|
|
func (s *Service) index(ctx context.Context, dep *store.Deployment) (*site.Index, error) {
|
|
files, err := s.DB.DeploymentFiles(ctx, dep.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return site.NewIndex(files), nil
|
|
}
|
|
|
|
// deploymentDir is where a deployment's tree was assembled, or "" under
|
|
// assemble_mode=none, where nothing was.
|
|
func (s *Service) deploymentDir(p *store.Project, dep *store.Deployment) string {
|
|
if s.Dir == "" {
|
|
return ""
|
|
}
|
|
return DeploymentDir(s.Dir, p.ID, dep.PublicID)
|
|
}
|
|
|
|
// LoadSites builds the registry from the database and makes the webroot agree
|
|
// with it. It runs once, before the listeners start.
|
|
//
|
|
// Without this a restart would serve 404 for every project until each one was
|
|
// deployed again. It also has to complete before the registry can stand in as
|
|
// the API's project resolver: a half-built registry would report a caller's own
|
|
// project as one that does not exist.
|
|
func (s *Service) LoadSites(ctx context.Context) error {
|
|
projects, err := s.DB.AllProjects(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
entries := make([]*site.Project, 0, len(projects))
|
|
var active int
|
|
for _, p := range projects {
|
|
sp := site.NewProject(p)
|
|
entries = append(entries, sp)
|
|
|
|
dep, err := s.DB.ActiveDeployment(ctx, p.ID)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
// A project that has never been deployed to. It exists, it
|
|
// resolves, and it answers 503 until something is activated.
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
idx, err := s.index(ctx, dep)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sp.Activate(site.NewDeployment(dep, idx, s.deploymentDir(p, dep)))
|
|
active++
|
|
}
|
|
s.Sites.Replace(entries)
|
|
s.Log.Info("site registry loaded", "projects", len(entries), "active", active)
|
|
|
|
// Best effort, like every other webroot operation: the server serves its own
|
|
// content and a stale symlink is not a reason to refuse to start.
|
|
if err := s.Reconcile(); err != nil {
|
|
s.Log.Warn("could not fully reconcile the webroot", "err", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Reconcile makes $WEBROOT agree with what this process is serving: one symlink
|
|
// per project with an active deployment, and nothing of ours left over for
|
|
// projects that no longer have one.
|
|
//
|
|
// The wanted set comes from the registry rather than from the database, because
|
|
// the registry is what requests are actually being answered from. The symlinks
|
|
// exist so that an external reader — a reverse proxy, a backup job — sees the
|
|
// same version this process does, and taking them from anywhere else would let
|
|
// the two disagree.
|
|
func (s *Service) Reconcile() error {
|
|
if s.Webroot == nil {
|
|
return nil
|
|
}
|
|
want := make(map[string]string)
|
|
for _, p := range s.Sites.Projects() {
|
|
// One load per project, and the value is used for both the name and the
|
|
// directory: reading Active() twice could straddle an activation and
|
|
// produce a link to a directory the other half of the pair disagrees with.
|
|
if d := p.Active(); d != nil && d.Dir != "" {
|
|
want[p.Name] = d.Dir
|
|
}
|
|
}
|
|
return s.Webroot.Reconcile(want)
|
|
}
|
|
|
|
// RunReconciler repairs the webroot on a timer until ctx is done.
|
|
//
|
|
// Activation repoints a symlink itself and logs when it cannot, so this is for
|
|
// the cases where nothing was there to notice: a link an operator deleted or
|
|
// edited, one whose repoint failed on a full disk, or one left pointing at a
|
|
// deployment that has since been collected. Serving does not depend on any of
|
|
// it, which is why a failure here is a warning and never stops the loop.
|
|
func (s *Service) RunReconciler(ctx context.Context, every time.Duration) {
|
|
if s.Webroot == nil || every <= 0 {
|
|
return
|
|
}
|
|
t := time.NewTicker(every)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
if err := s.Reconcile(); err != nil {
|
|
s.Log.Warn("could not fully reconcile the webroot", "err", err)
|
|
}
|
|
}
|
|
}
|
|
}
|