Add a metadata system

This commit is contained in:
Tom Wiesing 2022-09-20 13:11:24 +02:00
parent 07409a01be
commit 8b3218ad00
No known key found for this signature in database
16 changed files with 365 additions and 61 deletions

View file

@ -0,0 +1,204 @@
package instances
import (
"encoding/json"
"errors"
"github.com/FAU-CDI/wisski-distillery/internal/component/sql"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"gorm.io/gorm"
)
// MetaKey represents a key for metadata.
type MetaKey string
// ErrMetadatumNotSet is returned by various [MetaStorage] functions when a metadatum is not set
var ErrMetadatumNotSet = errors.New("metadatum not set")
// MetaStorage manages some metadata.
type MetaStorage interface {
// Get retrieves metadata with the provided key and deserializes the first one into target.
// If no metadatum exists, returns [ErrMetadatumNotSet].
Get(key MetaKey, target any) error
// GetAll receives all metadata with the provided keys.
// For each received value, the targets function is called with the current index, and total number of results.
// The function is intended to return a target for deserialization.
//
// When no metadatum exists, targets is not called, and nil error is returned.
GetAll(key MetaKey, targets func(index, total int) any) error
// Delete deletes all metadata with the provided key.
Delete(key MetaKey) error
// Set serializes value and stores it with the provided key.
// Any other metadata with the same key is deleted.
Set(key MetaKey, value any) error
// Add serializes values and stores each as associated with the provided key.
// Already existing metadata is left intact.
Add(key MetaKey, values ...any) error
// Purge removes all metadata, regardless of key.
Purge() error
}
// Metadata returns a system-wide [MetaStorage].
func (instances *Instances) Metadata() MetaStorage {
return &storage{
SQL: instances.SQL,
Slug: "", // not associated to any slug
}
}
// Metadata returns a [MetaStorage] that manages metadata related to this WissKI instance.
// It will be automatically deleted once the instance is deleted.
func (wisski *WissKI) Metadata() MetaStorage {
return &storage{
SQL: wisski.instances.SQL,
Slug: wisski.Slug, // associated to this instance
}
}
// storage implements MetaStorage
type storage struct {
SQL *sql.SQL
Slug string
}
func (s *storage) Get(key MetaKey, target any) error {
table, err := s.SQL.QueryTable(true, models.MetadataTable)
if err != nil {
return err
}
// read the datum from the database
var datum models.Metadatum
status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Order("pk DESC").Find(&datum)
// check if there was an error
if err := status.Error; err != nil {
return err
}
// check if e actually found it!
if status.RowsAffected == 0 {
return ErrMetadatumNotSet
}
// and do the unmarshaling!
return json.Unmarshal(datum.Value, target)
}
func (s *storage) GetAll(key MetaKey, target func(index, total int) any) error {
table, err := s.SQL.QueryTable(true, models.MetadataTable)
if err != nil {
return err
}
// read the datum from the database
var data []models.Metadatum
status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Find(&data)
// check if there was an error
if err := status.Error; err != nil {
return err
}
// unpack all of them into the destination
for index, datum := range data {
err := json.Unmarshal(datum.Value, target(index, len(data)))
if err != nil {
return err
}
}
return nil
}
func (s *storage) Delete(key MetaKey) error {
table, err := s.SQL.QueryTable(true, models.MetadataTable)
if err != nil {
return err
}
// delete all the values
status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{})
if err := status.Error; err != nil {
return err
}
return nil
}
func (s *storage) Set(key MetaKey, value any) error {
table, err := s.SQL.QueryTable(true, models.MetadataTable)
if err != nil {
return err
}
// marshal the value
bytes, err := json.Marshal(value)
if err != nil {
return err
}
return table.Transaction(func(tx *gorm.DB) error {
// delete the old values
status := tx.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{})
if err := status.Error; err != nil {
return err
}
// create the new item to insert
status = tx.Create(&models.Metadatum{
Key: string(key),
Slug: s.Slug,
Value: bytes,
})
if err := status.Error; err != nil {
return err
}
return nil
})
}
func (s *storage) Add(key MetaKey, values ...any) error {
table, err := s.SQL.QueryTable(true, models.MetadataTable)
if err != nil {
return err
}
return table.Transaction(func(tx *gorm.DB) error {
for _, value := range values {
bytes, err := json.Marshal(value)
if err != nil {
return err
}
// create the new item to insert
status := tx.Create(&models.Metadatum{
Key: string(key),
Slug: s.Slug,
Value: bytes,
})
if err := status.Error; err != nil {
return err
}
}
return nil
})
}
func (s *storage) Purge() error {
table, err := s.SQL.QueryTable(true, models.MetadataTable)
if err != nil {
return err
}
status := table.Where("slug = ?", s.Slug).Delete(&models.Metadatum{})
if status.Error != nil {
return status.Error
}
return nil
}

View file

@ -5,7 +5,6 @@ import (
"path/filepath"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
"github.com/alessio/shellescape"
"github.com/tkw1536/goprogram/stream"
@ -66,16 +65,10 @@ func (instances *Instances) Create(slug string) (wisski WissKI, err error) {
}
// Provision provisions an instance, assuming that the required databases already exist.
func (wisski WissKI) Provision(io stream.IOStream) error {
func (wisski *WissKI) Provision(io stream.IOStream) error {
// create the basic st!
st := wisski.Barrel()
if err := st.Install(wisski.instances.Core.Environment, io, component.InstallationContext{}); err != nil {
return err
}
// Pull and build the stack!
if err := st.Update(io, false); err != nil {
// build the container
if err := wisski.Build(io, false); err != nil {
return err
}
@ -106,7 +99,7 @@ func (wisski WissKI) Provision(io stream.IOStream) error {
// TODO: Move the provision script into the control plane!
provisionScript := "sudo PATH=$PATH -u www-data /bin/bash /provision_container.sh " + strings.Join(provisionParams, " ")
code, err := st.Run(io, true, "barrel", "/bin/bash", "-c", provisionScript)
code, err := wisski.Barrel().Run(io, true, "barrel", "/bin/bash", "-c", provisionScript)
if err != nil {
return err
}

View file

@ -3,15 +3,17 @@ package instances
import (
"embed"
"path/filepath"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/tkw1536/goprogram/stream"
)
//go:embed all:instances/barrel instances/barrel.env
var barrelResources embed.FS
// Barrel returns a stack representing the running WissKI Instance
func (wisski WissKI) Barrel() component.StackWithResources {
func (wisski *WissKI) Barrel() component.StackWithResources {
return component.StackWithResources{
Stack: component.Stack{
Dir: wisski.FilesystemBase,
@ -43,11 +45,59 @@ func (wisski WissKI) Barrel() component.StackWithResources {
}
}
const KeyLastRebuild MetaKey = "lastRebuild"
func (wisski *WissKI) LastRebuild() (t time.Time, err error) {
var epoch int64
// read the epoch!
err = wisski.Metadata().Get(KeyLastRebuild, &epoch)
if err == ErrMetadatumNotSet {
return t, nil
}
if err != nil {
return t, err
}
// and turn it into time!
return time.Unix(epoch, 0), nil
}
func (wisski *WissKI) setLastRebuild() error {
return wisski.Metadata().Set(KeyLastRebuild, time.Now().Unix())
}
// Build builds or rebuilds the barel connected to this instance.
//
// It also logs the current time into the metadata belonging to this instance.
func (wisski *WissKI) Build(stream stream.IOStream, start bool) error {
barrel := wisski.Barrel()
var context component.InstallationContext
{
err := barrel.Install(stream, context)
if err != nil {
return err
}
}
{
err := barrel.Update(stream, start)
if err != nil {
return err
}
}
// store the current last rebuild
return wisski.setLastRebuild()
}
//go:embed all:instances/reserve instances/reserve.env
var reserveResources embed.FS
// Reserve returns a stack representing the reserve instance
func (wisski WissKI) Reserve() component.StackWithResources {
func (wisski *WissKI) Reserve() component.StackWithResources {
return component.StackWithResources{
Stack: component.Stack{
Dir: wisski.FilesystemBase,

View file

@ -1,6 +1,9 @@
package instances
import (
"fmt"
"time"
"github.com/tkw1536/goprogram/stream"
"golang.org/x/sync/errgroup"
)
@ -10,12 +13,15 @@ type Info struct {
Slug string // The slug of the instance
URL string // The public URL of this instance
LastRebuild time.Time
Running bool // is the instance running?
Pathbuilders []string // list of pathbuilders
}
// Info returns information about this WissKI instance.
func (wisski *WissKI) Info(quick bool) (info Info, err error) {
fmt.Println("call to info")
// static properties
info.Slug = wisski.Slug
info.URL = wisski.URL().String()
@ -30,15 +36,20 @@ func (wisski *WissKI) Info(quick bool) (info Info, err error) {
})
// slower checks for extra properties.
// these execute php code
// these might execute php code or require additional database queries.
if !quick {
group.Go(func() error {
info.Pathbuilders, _ = wisski.Pathbuilders()
return nil
})
group.Go(func() (err error) {
info.Pathbuilders, err = wisski.Pathbuilders()
return
info.LastRebuild, _ = wisski.LastRebuild()
return nil
})
}
err = group.Wait()
fmt.Println(err)
return
}