init
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user