Add support for provisioning and rebuilding via interface

This commit is contained in:
Tom 2023-07-09 11:18:14 +02:00
parent f5c5999f44
commit ddb4bb3546
76 changed files with 1306 additions and 625 deletions

2
API.md
View file

@ -25,6 +25,8 @@ All routes can be found under `/api/v1/http/`
## Interactive Websocket API ## Interactive Websocket API
** This is not yet implemented in it's entirety **
Some API calls require interactivity or provide streaming content to clients. Some API calls require interactivity or provide streaming content to clients.
An example of such an action is creating a new instance. An example of such an action is creating a new instance.
The protocol is based on [Websockets](https://websockets.spec.whatwg.org/). The protocol is based on [Websockets](https://websockets.spec.whatwg.org/).

View file

@ -4,6 +4,7 @@ import (
wisski_distillery "github.com/FAU-CDI/wisski-distillery" wisski_distillery "github.com/FAU-CDI/wisski-distillery"
"github.com/FAU-CDI/wisski-distillery/internal/cli" "github.com/FAU-CDI/wisski-distillery/internal/cli"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/FAU-CDI/wisski-distillery/pkg/logging"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
) )
@ -13,6 +14,7 @@ var Provision wisski_distillery.Command = pv{}
type pv struct { type pv struct {
PHPVersion string `short:"p" long:"php" description:"specific php version to use for instance. Should be one of '8.0', '8.1'."` PHPVersion string `short:"p" long:"php" description:"specific php version to use for instance. Should be one of '8.0', '8.1'."`
OPCacheDevelopment bool `short:"o" long:"opcache-devel" description:"Include opcache development configuration"`
Positionals struct { Positionals struct {
Slug string `positional-arg-name:"slug" required:"1-1" description:"slug of instance to create"` Slug string `positional-arg-name:"slug" required:"1-1" description:"slug of instance to create"`
} `positional-args:"true"` } `positional-args:"true"`
@ -36,9 +38,12 @@ var errProvisionGeneric = exit.Error{
// TODO: AfterParse to check instance! // TODO: AfterParse to check instance!
func (p pv) Run(context wisski_distillery.Context) error { func (p pv) Run(context wisski_distillery.Context) error {
instance, err := context.Environment.Provision().Provision(context.Stderr, context.Context, provision.ProvisionFlags{ instance, err := context.Environment.Provision().Provision(context.Stderr, context.Context, provision.Flags{
Slug: p.Positionals.Slug, Slug: p.Positionals.Slug,
PHPVersion: p.PHPVersion, System: models.System{
PHP: p.PHPVersion,
OpCacheDevelopment: p.OPCacheDevelopment,
},
}) })
if err != nil { if err != nil {
return errProvisionGeneric.WithMessageF(p.Positionals.Slug).Wrap(err) return errProvisionGeneric.WithMessageF(p.Positionals.Slug).Wrap(err)

View file

@ -6,6 +6,7 @@ import (
wisski_distillery "github.com/FAU-CDI/wisski-distillery" wisski_distillery "github.com/FAU-CDI/wisski-distillery"
"github.com/FAU-CDI/wisski-distillery/internal/cli" "github.com/FAU-CDI/wisski-distillery/internal/cli"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/pkglib/status" "github.com/tkw1536/pkglib/status"
@ -15,8 +16,10 @@ import (
var Rebuild wisski_distillery.Command = rebuild{} var Rebuild wisski_distillery.Command = rebuild{}
type rebuild struct { type rebuild struct {
Parallel int `short:"p" long:"parallel" description:"run on (at most) this many instances in parallel. 0 for no limit." default:"1"` Parallel int `short:"a" long:"parallel" description:"run on (at most) this many instances in parallel. 0 for no limit." default:"1"`
PHPVersion string `short:"u" long:"php" description:"update to specific php version to use for instance. Should be one of '8.0', '8.1'."`
PHPVersion string `short:"p" long:"php" description:"update to specific php version to use for instance. Should be one of '8.0', '8.1'."`
OPCacheDevelopment bool `short:"o" long:"opcache-devel" description:"Include opcache development configuration"`
Positionals struct { Positionals struct {
Slug []string `positional-arg-name:"SLUG" required:"0" description:"slug of instance or instances to run rebuild"` Slug []string `positional-arg-name:"SLUG" required:"0" description:"slug of instance or instances to run rebuild"`
} `positional-args:"true"` } `positional-args:"true"`
@ -50,13 +53,10 @@ func (rb rebuild) Run(context wisski_distillery.Context) (err error) {
// and do the actual rebuild // and do the actual rebuild
return status.WriterGroup(context.Stderr, rb.Parallel, func(instance *wisski.WissKI, writer io.Writer) error { return status.WriterGroup(context.Stderr, rb.Parallel, func(instance *wisski.WissKI, writer io.Writer) error {
if rb.PHPVersion != "" { return instance.SystemManager().Apply(context.Context, writer, models.System{
if err := instance.Provisioner().ApplyFlags(context.Context, writer, rb.PHPVersion); err != nil { PHP: rb.PHPVersion,
return err OpCacheDevelopment: rb.OPCacheDevelopment,
} }, true)
}
return instance.Barrel().Build(context.Context, writer, true)
}, wissKIs, status.SmartMessage(func(item *wisski.WissKI) string { }, wissKIs, status.SmartMessage(func(item *wisski.WissKI) string {
return fmt.Sprintf("rebuild %q", item.Slug) return fmt.Sprintf("rebuild %q", item.Slug)
})) }))

View file

@ -4,6 +4,7 @@ import (
wisski_distillery "github.com/FAU-CDI/wisski-distillery" wisski_distillery "github.com/FAU-CDI/wisski-distillery"
"github.com/FAU-CDI/wisski-distillery/internal/cli" "github.com/FAU-CDI/wisski-distillery/internal/cli"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/FAU-CDI/wisski-distillery/pkg/logging"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/pkglib/fsx" "github.com/tkw1536/pkglib/fsx"
@ -53,7 +54,7 @@ func (r reserve) Run(context wisski_distillery.Context) (err error) {
} }
// make it in-memory // make it in-memory
instance, err := dis.Instances().Create(slug, "") instance, err := dis.Instances().Create(slug, models.System{})
if err != nil { if err != nil {
return errProvisionGeneric.WithMessageF(slug, err) return errProvisionGeneric.WithMessageF(slug, err)
} }

View file

@ -5,6 +5,7 @@ import (
wisski_distillery "github.com/FAU-CDI/wisski-distillery" wisski_distillery "github.com/FAU-CDI/wisski-distillery"
"github.com/FAU-CDI/wisski-distillery/internal/cli" "github.com/FAU-CDI/wisski-distillery/internal/cli"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/goprogram/parser" "github.com/tkw1536/goprogram/parser"
) )
@ -43,12 +44,16 @@ func (sh shell) Run(context wisski_distillery.Context) error {
return errShellWissKI.Wrap(err) return errShellWissKI.Wrap(err)
} }
code := instance.Barrel().Shell(context.Context, context.IOStream, sh.Positionals.Args...)() {
if code != 0 { err := instance.Barrel().Shell(context.Context, context.IOStream, sh.Positionals.Args...)
if err != nil {
code := err.(barrel.ExitError).Code()
return exit.Error{ return exit.Error{
ExitCode: exit.ExitCode(uint8(code)), ExitCode: code,
Message: fmt.Sprintf("Exit code %d", code), Message: fmt.Sprintf("Exit code %d", code),
} }
} }
}
return nil return nil
} }

View file

@ -1,7 +1,7 @@
package cli package cli
// =========================================================================================================== // ===========================================================================================================
// This file was generated automatically at 10-07-2023 22:16:29 using gogenlicense. // This file was generated automatically at 13-07-2023 07:44:29 using gogenlicense.
// Do not edit manually, as changes may be overwritten. // Do not edit manually, as changes may be overwritten.
// =========================================================================================================== // ===========================================================================================================
@ -4913,7 +4913,7 @@ package cli
// # Generation // # Generation
// //
// This variable and the associated documentation have been automatically generated using the 'gogenlicense' tool. // This variable and the associated documentation have been automatically generated using the 'gogenlicense' tool.
// It was last updated at 10-07-2023 22:16:29. // It was last updated at 13-07-2023 07:44:29.
var LegalNotices string var LegalNotices string
func init() { func init() {

View file

@ -19,7 +19,7 @@ var (
// It validates that slug is a valid name for an instance. // It validates that slug is a valid name for an instance.
// //
// It does not perform any checks if the instance already exists, or does the creation in the database. // It does not perform any checks if the instance already exists, or does the creation in the database.
func (instances *Instances) Create(slug string, phpversion string) (wissKI *wisski.WissKI, err error) { func (instances *Instances) Create(slug string, system models.System) (wissKI *wisski.WissKI, err error) {
// make sure that the slug is valid! // make sure that the slug is valid!
slug, err = instances.IsValidSlug(slug) slug, err = instances.IsValidSlug(slug)
@ -66,7 +66,7 @@ func (instances *Instances) Create(slug string, phpversion string) (wissKI *wiss
} }
// docker image // docker image
wissKI.Liquid.Instance.DockerBaseImage, err = models.GetBaseImage(phpversion) wissKI.Liquid.Instance.System = system
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/FAU-CDI/wisski-distillery/pkg/logging"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
) )
@ -37,7 +38,7 @@ func (purger *Purger) Purge(ctx context.Context, out io.Writer, slug string) err
instance, err := purger.Dependencies.Instances.WissKI(ctx, slug) instance, err := purger.Dependencies.Instances.WissKI(ctx, slug)
if err == instances.ErrWissKINotFound { if err == instances.ErrWissKINotFound {
fmt.Fprintln(out, "Not found in bookkeeping table, assuming defaults") fmt.Fprintln(out, "Not found in bookkeeping table, assuming defaults")
instance, err = purger.Dependencies.Instances.Create(slug, "") instance, err = purger.Dependencies.Instances.Create(slug, models.System{})
} }
if err != nil { if err != nil {
return errPurgeNoDetails.WithMessageF(err) return errPurgeNoDetails.WithMessageF(err)

View file

@ -1,8 +0,0 @@
#!/bin/bash
# This utility script can be used to run all cron tasks.
cd /var/www/data/project || exit 1
export PATH=/var/www/data/project/vendor/bin:$PATH
drush core-cron

View file

@ -1,6 +0,0 @@
#!/bin/sh
# This script can be used to repatch EasyRDF when needed.
cd /var/www/data/project/web/modules/contrib/wisski || exit 1
EASYRDF_RESPONSE="./vendor/easyrdf/easyrdf/lib/EasyRdf/Http/Response.php"
patch -N "$EASYRDF_RESPONSE" < "/patch/easyrdf.patch"

View file

@ -1,6 +0,0 @@
#!/bin/sh
# This script can be used to repatch EasyRDF when needed.
cd /var/www/data/project/web/modules/contrib/wisski/ || exit 1
TRIPLESTABCONTROLLER="./wisski_adapter_sparql11_pb/src/Controller/Sparql11TriplesTabController.php"
patch -N "$TRIPLESTABCONTROLLER" < "/patch/triples.patch"

View file

@ -18,7 +18,6 @@ popd || exit 1
# run the update and clear the cache! # run the update and clear the cache!
drush updatedb --yes drush updatedb --yes
# drush cc
# and reset everything back to normal # and reset everything back to normal
chmod 755 web/sites/default chmod 755 web/sites/default

View file

@ -3,12 +3,14 @@ package provision
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/manager"
"github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/FAU-CDI/wisski-distillery/pkg/logging"
"github.com/tkw1536/pkglib/fsx" "github.com/tkw1536/pkglib/fsx"
) )
@ -21,39 +23,46 @@ type Provision struct {
} }
} }
// ProvisionFlags are flags for a new instance // Flags are flags for a new instance.
type ProvisionFlags struct { type Flags struct {
// NOTE(twiesing): Any changes here should be reflected in instance_provision.html and remote/api.ts.
// Slug is the slug of the wisski instance // Slug is the slug of the wisski instance
Slug string Slug string
// PHP Version to use // System is information about the system
PHPVersion string System models.System
}
// Profile returns the profile belonging to this provision flags.
func (flags Flags) Profile() manager.Profile {
// TODO: Actually do something here
return manager.Profile{}
} }
var ErrInstanceAlreadyExists = errors.New("instance with provided slug already exists") var ErrInstanceAlreadyExists = errors.New("instance with provided slug already exists")
func (pv *Provision) ValidateFlags(flags ProvisionFlags) error { func (pv *Provision) Validate(flags Flags) error {
// check the slug // check the slug
if _, err := pv.Dependencies.Instances.IsValidSlug(flags.Slug); err != nil { if _, err := pv.Dependencies.Instances.IsValidSlug(flags.Slug); err != nil {
return err return err
} }
// check for known php versions
if _, err := models.GetBaseImage(flags.PHPVersion); err != nil {
return err
}
return nil return nil
} }
// Provision provisions a new docker compose instance. // Provision provisions a new docker compose instance.
func (pv *Provision) Provision(progress io.Writer, ctx context.Context, flags ProvisionFlags) (*wisski.WissKI, error) { func (pv *Provision) Provision(progress io.Writer, ctx context.Context, flags Flags) (*wisski.WissKI, error) {
// check that it doesn't already exist // check that it doesn't already exist
logging.LogMessage(progress, "Provisioning new WissKI instance %s", flags.Slug) logging.LogMessage(progress, "Provisioning new WissKI instance %s", flags.Slug)
if exists, err := pv.Dependencies.Instances.Has(ctx, flags.Slug); err != nil || exists { if exists, err := pv.Dependencies.Instances.Has(ctx, flags.Slug); err != nil || exists {
return nil, ErrInstanceAlreadyExists return nil, ErrInstanceAlreadyExists
} }
// log out what we're doing!
fmt.Fprintf(progress, "%#v", flags)
// make it in-memory // make it in-memory
instance, err := pv.Dependencies.Instances.Create(flags.Slug, flags.PHPVersion) instance, err := pv.Dependencies.Instances.Create(flags.Slug, flags.System)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -99,7 +108,7 @@ func (pv *Provision) Provision(progress io.Writer, ctx context.Context, flags Pr
// run the provision script // run the provision script
if err := logging.LogOperation(func() error { if err := logging.LogOperation(func() error {
return instance.Provisioner().Provision(ctx, progress) return instance.Manager().Provision(ctx, progress, flags.System, flags.Profile())
}, progress, "Running setup scripts"); err != nil { }, progress, "Running setup scripts"); err != nil {
return nil, err return nil, err
} }

View file

@ -75,6 +75,7 @@ var (
menuInstances = component.MenuItem{Title: "Instances", Path: "/admin/instances/"} menuInstances = component.MenuItem{Title: "Instances", Path: "/admin/instances/"}
menuInstance = component.DummyMenuItem() menuInstance = component.DummyMenuItem()
menuRebuild = component.DummyMenuItem()
menuGrants = component.DummyMenuItem() menuGrants = component.DummyMenuItem()
menuIngredients = component.DummyMenuItem() menuIngredients = component.DummyMenuItem()
) )
@ -146,6 +147,11 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
router.Handler(http.MethodGet, route+"instance/:slug", instance) router.Handler(http.MethodGet, route+"instance/:slug", instance)
} }
{
rebuild := admin.instanceRebuild(ctx)
router.Handler(http.MethodGet, route+"rebuild/:slug", rebuild)
}
{ {
grants := admin.grants(ctx) grants := admin.grants(ctx)
router.Handler(http.MethodGet, route+"grants/:slug", grants) router.Handler(http.MethodGet, route+"grants/:slug", grants)

View file

@ -139,7 +139,7 @@ func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (funcs
// replace the functions // replace the functions
funcs = []templating.FlagFunc{ funcs = []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}), templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceCrumb(menuGrants, component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}), templating.ReplaceCrumb(menuGrants, component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}),
templating.Title(gc.Instance.Slug + " - Grants"), templating.Title(gc.Instance.Slug + " - Grants"),
} }
return funcs, nil return funcs, nil

View file

@ -84,6 +84,48 @@
</div> </div>
</div> </div>
</div> </div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
System
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
PHP Version
</td>
<td>
<code>{{ .Instance.System.PHP }}</code>
</td>
</tr>
<tr>
<td>
Docker Base Image
</td>
<td>
<code>{{ .Instance.System.GetDockerBaseImage }}</code>
</td>
</tr>
<tr>
<td>
OPCache Development Config
</td>
<td>
<code>{{ .Instance.System.OpCacheDevelopment }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5"> <div class="pure-u-1 pure-u-xl-2-5">
<div class="padding"> <div class="padding">
<div class="overflow"> <div class="overflow">
@ -182,7 +224,7 @@
<tr> <tr>
<td> <td>
Last Rebuild <br> Last Rebuild <br>
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Rebuild</button> <a class="pure-button" href="/admin/rebuild/{{ .Info.Slug }}">Rebuild</button>
</td> </td>
<td> <td>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code> <code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>

View file

@ -1,11 +1,38 @@
<div class="pure-u-1-1"> <div class="pure-u-1-1">
<form class="pure-form pure-form-aligned" id="provision"> <form class="pure-form pure-form-aligned" id="provision">
<fieldset disabled="disabled"> <fieldset>
<legend>Main Parameters</legend>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="slug">Slug</label> <label for="slug">Slug</label>
<input name="slug" id="slug" placeholder="" autocomplete="slug"> <input name="slug" id="slug" placeholder="" autocomplete="slug">
</div> </div>
<input type="submit" value="Provision" class="pure-button">
</fieldset> </fieldset>
<fieldset>
<legend>System Parameters</legend>
<div class="pure-control-group">
<label for="php">PHP Version</label>
<select id="php">
<option value="" selected>Default ({{ .DefaultPHPVersion }})</option>
{{ range .PHPVersions }}
<option value="{{ . }}">{{ . }}</option>
{{ end }}
</select>
</div>
<label for="opcacheDevelopment" class="pure-checkbox">
<input type="checkbox" id="opcacheDevelopment" />
Opache Development Configuration
</label>
</fieldset>
<fieldset>
<legend>Profile</legend>
<div class="pure-control-group">
Not yet available
</div>
</fieldset>
<input type="submit" value="Provision" class="pure-button">
</form> </form>
</div> </div>

View file

@ -0,0 +1,39 @@
<div class="pure-u-1-1">
<form class="pure-form pure-form-aligned" id="provision">
<fieldset>
<legend>Main Parameters</legend>
<div class="pure-control-group">
<label for="slug">Slug</label>
<input name="slug" id="slug" placeholder="" autocomplete="slug" readonly="readonly" value="{{ .Slug }}">
</div>
</fieldset>
<fieldset>
<legend>System Parameters</legend>
<div class="pure-control-group">
<label for="php">PHP Version</label>
<select id="php">
{{ $PHP := .System.PHP }}
<option {{ if eq $PHP "" }}selected{{ end }}>Default ({{ .DefaultPHPVersion }})</option>
{{ range .PHPVersions }}
<option {{ if eq $PHP . }}selected{{ end }} value="{{ . }}">{{ . }}</option>
{{ end }}
</select>
</div>
<label for="opcacheDevelopment" class="pure-checkbox">
<input {{ if .System.OpCacheDevelopment }}checked{{end}} type="checkbox" id="opcacheDevelopment" check/>
Opache Development Configuration
</label>
</fieldset>
<fieldset>
<legend>Profile</legend>
<div class="pure-control-group">
Not yet available
</div>
</fieldset>
<input type="submit" value="Rebuild" class="pure-button">
</form>
</div>

View file

@ -6,6 +6,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
_ "embed" _ "embed"
) )
@ -22,7 +23,18 @@ var instanceProvisionTemplate = templating.Parse[instanceProvisionContext](
type instanceProvisionContext struct { type instanceProvisionContext struct {
templating.RuntimeFlags templating.RuntimeFlags
// nothing for the moment systemParams
}
type systemParams struct {
PHPVersions []string
DefaultPHPVersion string
}
func newSystemParams() (sp systemParams) {
sp.PHPVersions = models.KnownPHPVersions()
sp.DefaultPHPVersion = models.DefaultPHPVersion
return sp
} }
func (admin *Admin) instanceProvision(ctx context.Context) http.Handler { func (admin *Admin) instanceProvision(ctx context.Context) http.Handler {
@ -37,6 +49,7 @@ func (admin *Admin) instanceProvision(ctx context.Context) http.Handler {
) )
return tpl.HTMLHandler(func(r *http.Request) (ipc instanceProvisionContext, err error) { return tpl.HTMLHandler(func(r *http.Request) (ipc instanceProvisionContext, err error) {
ipc.systemParams = newSystemParams()
return ipc, nil return ipc, nil
}) })
} }

View file

@ -0,0 +1,74 @@
package admin
import (
"context"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/julienschmidt/httprouter"
"github.com/tkw1536/pkglib/httpx"
_ "embed"
)
//go:embed "html/instance_rebuild.html"
var instanceRebuildHTML []byte
var instanceRebuildTemplate = templating.Parse[instanceRebuildContext](
"instance_rebuild.html", instanceRebuildHTML, nil,
templating.Title("Rebuild Instance"),
templating.Assets(assets.AssetsAdminRebuild),
)
type instanceRebuildContext struct {
templating.RuntimeFlags
Slug string
System models.System
systemParams
}
func (admin *Admin) instanceRebuild(ctx context.Context) http.Handler {
tpl := instanceRebuildTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
menuAdmin,
menuInstances,
menuInstance,
menuRebuild,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ib instanceRebuildContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
var instance *wisski.WissKI
instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound {
return ib, nil, httpx.ErrNotFound
}
if err != nil {
return ib, nil, err
}
ib.Slug = instance.Slug
ib.System = instance.System
// replace the menu item
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + instance.Slug)}),
templating.ReplaceCrumb(menuRebuild, component.MenuItem{Title: "Rebuild", Path: template.URL("/admin/rebuild/" + instance.Slug)}),
templating.Title(instance.Slug + " - Rebuild"),
}
ib.systemParams = newSystemParams()
return
})
}

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/internal/wisski"
) )
@ -28,7 +29,7 @@ func (sockets *Sockets) Actions() ActionMap {
}), }),
"provision": sockets.Generic(1, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error { "provision": sockets.Generic(1, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error {
// read the flags of the instance to be provisioned // read the flags of the instance to be provisioned
var flags provision.ProvisionFlags var flags provision.Flags
if err := json.Unmarshal([]byte(params[0]), &flags); err != nil { if err := json.Unmarshal([]byte(params[0]), &flags); err != nil {
return err return err
} }
@ -63,8 +64,13 @@ func (sockets *Sockets) Actions() ActionMap {
}, },
) )
}), }),
"rebuild": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "rebuild": sockets.Instance(1, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Barrel().Build(ctx, out, true) // read the flags of the instance to be provisioned
var system models.System
if err := json.Unmarshal([]byte(params[0]), &system); err != nil {
return err
}
return instance.SystemManager().Apply(ctx, out, system, true)
}), }),
"update": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { "update": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Drush().Update(ctx, out) return instance.Drush().Update(ctx, out)

View file

@ -21,4 +21,4 @@ type Assets struct {
Styles template.HTML // <link> tags inserted by the asset Styles template.HTML // <link> tags inserted by the asset
} }
//go:generate node build.mjs Default User Admin AdminProvision //go:generate node build.mjs Default User Admin AdminProvision AdminRebuild

View file

@ -24,12 +24,18 @@ var AssetsUser = Assets{
// AssetsAdmin contains assets for the 'Admin' entrypoint. // AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{ var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/Admin.205f0180.js"></script><script src="/⛰/Admin.59fb2e50.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script src="/⛰/Admin.6daf9fdd.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css">`, Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css">`,
} }
// AssetsAdminProvision contains assets for the 'AdminProvision' entrypoint. // AssetsAdminProvision contains assets for the 'AdminProvision' entrypoint.
var AssetsAdminProvision = Assets{ var AssetsAdminProvision = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.59fb2e50.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.205f0180.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminProvision.3cf9e19e.js"></script><script src="/⛰/AdminProvision.d195fd59.js" nomodule="" defer></script>`, Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.6daf9fdd.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminProvision.b7679968.js"></script><script src="/⛰/AdminProvision.12a47f22.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminProvision.38d394c2.css">`, Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminProvision.38d394c2.css">`,
} }
// AssetsAdminRebuild contains assets for the 'AdminRebuild' entrypoint.
var AssetsAdminRebuild = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.6daf9fdd.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminRebuild.330247d9.js"></script><script src="/⛰/AdminRebuild.527a9616.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminRebuild.38d394c2.css">`,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var r={id:e,exports:{}};return n[e]=r,t.call(r.exports,r,r.exports),r.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("dK5Bi");var r,l=t("8vh0V");async function i(e){return new Promise(((n,o)=>{(0,l.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(t,r)=>{t?n(e.Slug):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("provision"),a=document.getElementById("slug"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");d.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:u.value,OpCacheDevelopment:c.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=d.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var r={id:e,exports:{}};return n[e]=r,t.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("8xGhL");var r=t("12vpF");const i=document.getElementById("provision"),l=document.getElementById("slug");i.addEventListener("submit",(e=>{e.preventDefault();const n={Slug:l.value};(0,r.createModal)("provision",[JSON.stringify(n)],{bufferSize:0,onClose:e=>{e?location.href="/admin/instance/"+n.Slug:location.reload()}})})),i.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e){return new Promise(((n,t)=>{(0,r.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(o,r)=>{o?n(e.Slug):t(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("provision"),a=document.getElementById("slug"),d=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");l.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:d.value,OpCacheDevelopment:u.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),l.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -1 +0,0 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var i={id:e,exports:{}};return n[e]=i,t.call(i.exports,i,i.exports),i.exports}var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("dK5Bi");var i,r=t("8vh0V");const l=document.getElementById("provision"),d=document.getElementById("slug");l.addEventListener("submit",(e=>{e.preventDefault();const n={Slug:d.value};(0,r.createModal)("provision",[JSON.stringify(n)],{bufferSize:0,onClose:e=>{e?location.href="/admin/instance/"+n.Slug:location.reload()}})})),null===(i=l.querySelector("fieldset"))||void 0===i||i.removeAttribute("disabled")}();

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e,n){return new Promise(((t,o)=>{(0,r.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("slug"),d=document.getElementById("provision"),a=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");d.addEventListener("submit",(e=>{e.preventDefault(),i(l.value,{PHP:a.value,OpCacheDevelopment:u.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),d.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("dK5Bi");var r,l=o("8vh0V");async function i(e,n){return new Promise(((t,o)=>{(0,l.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("slug"),a=document.getElementById("provision"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");a.addEventListener("submit",(e=>{e.preventDefault(),i(d.value,{PHP:u.value,OpCacheDevelopment:c.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=a.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -1,30 +1,22 @@
import "../Admin/index.ts" import "../Admin/index.ts"
import "../Admin/index.css" import "../Admin/index.css"
import { createModal } from "~/src/lib/remote" import { Provision } from "~/src/lib/remote/api"
const provision = document.getElementById("provision") as HTMLFormElement; const provision = document.getElementById("provision") as HTMLFormElement;
const slug = document.getElementById("slug") as HTMLInputElement; const slug = document.getElementById("slug") as HTMLInputElement;
const php = document.getElementById("php") as HTMLSelectElement;
const opcacheDevelopment = document.getElementById("opcacheDevelopment") as HTMLInputElement;
// add an event handler to open the modal form! // add an event handler to open the modal form!
provision.addEventListener('submit', (evt) => { provision.addEventListener('submit', (evt) => {
evt.preventDefault(); evt.preventDefault();
// flags used to create the server Provision({ Slug: slug.value, System: { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked } })
const flags = { Slug: slug.value }; .then(slug => {
location.href = "/admin/instance/" + slug;
// open a modal to provision a new instance
createModal("provision", [JSON.stringify(flags)], {
bufferSize: 0,
onClose: (success: boolean) => {
if (success) {
location.href = "/admin/instance/" + flags.Slug
} else {
location.reload();
}
},
}) })
.catch((e) => {console.error(e); location.reload()});
}) })
// enable the form! // enable the form!

View file

@ -0,0 +1,24 @@
import "../Admin/index.ts"
import "../Admin/index.css"
import { Rebuild } from "~/src/lib/remote/api"
const slug = document.getElementById("slug") as HTMLInputElement
const provision = document.getElementById("provision") as HTMLFormElement;
const php = document.getElementById("php") as HTMLSelectElement;
const opcacheDevelopment = document.getElementById("opcacheDevelopment") as HTMLInputElement;
// add an event handler to open the modal form!
provision.addEventListener('submit', (evt) => {
evt.preventDefault();
Rebuild(slug.value, { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked })
.then(slug => {
location.href = "/admin/instance/" + slug;
})
.catch((e) => {console.error(e); location.reload()});
})
// enable the form!
provision.querySelector('fieldset')?.removeAttribute('disabled');

View file

@ -0,0 +1,50 @@
import { createModal } from "~/src/lib/remote"
/**
* Flags to provision a new system.
* Should mirror "provision".Flags.
*/
interface ProvisionFlags {
Slug: string
System: System
}
interface System {
PHP: string;
OpCacheDevelopment: boolean
}
/** Rebuild the specified instance */
export async function Rebuild(slug: string, system: System): Promise<string> {
return new Promise((rs, rj) => {
createModal("rebuild", [slug, JSON.stringify(system)], {
bufferSize: 0,
onClose: (success: boolean, message?: string) => {
if (!success) {
rj(new Error(message ?? "unspecified error"))
return;
}
rs(slug);
},
})
});
}
/** Provision provisions a new instance */
export async function Provision(flags: ProvisionFlags): Promise<string> {
// open a modal to provision a new instance
return new Promise((rs, rj) => {
createModal("provision", [JSON.stringify(flags)], {
bufferSize: 0,
onClose: (success: boolean, message?: string) => {
if (!success) {
rj(new Error(message ?? "unspecified error"))
return;
}
rs(flags.Slug);
},
})
});
}

View file

@ -105,7 +105,7 @@ export default function setup() {
type ModalOptions = { type ModalOptions = {
bufferSize: number; bufferSize: number;
onClose: (success: boolean) => void onClose: (success: boolean, message?: string) => void
} }
export function createModal(action: string, params: string[], opts: Partial<ModalOptions>) { export function createModal(action: string, params: string[], opts: Partial<ModalOptions>) {
// create a modal dialog and append it to the body // create a modal dialog and append it to the body
@ -123,14 +123,14 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
finishButton.className = "pure-button pure-button-success" finishButton.className = "pure-button pure-button-success"
finishButton.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close") finishButton.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close")
let result = {success: false, error: "unknown error"}; let result: ResultMessage = {success: false};
finishButton.addEventListener('click', (event) => { finishButton.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();
if (typeof opts?.onClose === 'function') { if (typeof opts?.onClose === 'function') {
finishButton.setAttribute('disabled', 'disabled') finishButton.setAttribute('disabled', 'disabled')
target.innerHTML = 'Finishing up ...' target.innerHTML = 'Finishing up ...'
opts.onClose(result.success) opts.onClose(result.success, result.message)
return; return;
} }
@ -147,7 +147,9 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
window.onbeforeunload = () => "A remote session is in progress. Are you sure you want to leave?"; window.onbeforeunload = () => "A remote session is in progress. Are you sure you want to leave?";
// when closing, add a button to the modal! // when closing, add a button to the modal!
const close = (result: ResultMessage) => { const close = (message: ResultMessage) => {
result = message
if (result.success) { if (result.success) {
println('Process completed successfully. ', true); println('Process completed successfully. ', true);
} else { } else {
@ -168,6 +170,7 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
// connect to the socket and send the action // connect to the socket and send the action
callServerAction( callServerAction(
location.href.replace('http', 'ws'),
{ {
'name': action, 'name': action,
'params': params, 'params': params,

View file

@ -11,12 +11,14 @@ function isResultMessage(value: any): value is ResultMessage {
/** /**
* Opens a WebSocket connection and calls a server action * Opens a WebSocket connection and calls a server action
* @param endpoint Endpoint to call
* @param call Function to call * @param call Function to call
* @param onOpen callback for once the connection is opened. The send function can be used to send additional text to the server. * @param onOpen callback for once the connection is opened. The send function can be used to send additional text to the server.
* @param onText called when the connection receives some text * @param onText called when the connection receives some text
* @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors. * @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors.
*/ */
export default async function callServerAction( export default async function callServerAction(
endpoint: string,
call: CallMessage, call: CallMessage,
onOpen: (send: (text: string) => void, cancel: () => void) => void, onOpen: (send: (text: string) => void, cancel: () => void) => void,
onText: (text: string) => void, onText: (text: string) => void,
@ -24,7 +26,7 @@ export default async function callServerAction(
return new Promise((rs, rj) => { return new Promise((rs, rj) => {
const mutex = new Mutex(); const mutex = new Mutex();
const socket = new WebSocket(location.href.replace('http', 'ws')); const socket = new WebSocket(endpoint);
let result: ResultMessage; let result: ResultMessage;
socket.onmessage = (msg) => { socket.onmessage = (msg) => {

View file

@ -88,18 +88,27 @@ func (ds Stack) Exec(ctx context.Context, io stream.IOStream, service, executabl
return ds.compose(ctx, io, compose...) return ds.compose(ctx, io, compose...)
} }
type RunFlags struct {
AutoRemove bool
Detach bool
}
// Run runs a command in a running container with the given executable. // Run runs a command in a running container with the given executable.
// It is equivalent to 'docker compose run [--rm] $service $executable $args...'. // It is equivalent to 'docker compose run [--rm] $service $executable $args...'.
// //
// It returns the exit code of the process. // It returns the exit code of the process.
func (ds Stack) Run(ctx context.Context, io stream.IOStream, autoRemove bool, service, command string, args ...string) (int, error) { func (ds Stack) Run(ctx context.Context, io stream.IOStream, flags RunFlags, service, command string, args ...string) (int, error) {
compose := []string{"run"} compose := []string{"run"}
if autoRemove { if flags.AutoRemove {
compose = append(compose, "--rm") compose = append(compose, "--rm")
} }
if !io.StdinIsATerminal() { if !io.StdinIsATerminal() {
compose = append(compose, "-T") compose = append(compose, "--no-TTY")
} }
if flags.Detach {
compose = append(compose, "--detach")
}
compose = append(compose, service, command) compose = append(compose, service, command)
compose = append(compose, args...) compose = append(compose, args...)
@ -131,6 +140,16 @@ func (ds Stack) Down(ctx context.Context, progress io.Writer) error {
return nil return nil
} }
// DownAll stops and removes all containers in this Stack, and those not defined in the compose file.
// It is equivalent to 'docker compose down -v --remove-orphans' on the shell.
func (ds Stack) DownAll(ctx context.Context, progress io.Writer) error {
code := ds.compose(ctx, stream.NonInteractive(progress), "down", "-v", "--remove-orphans")()
if code != 0 {
return errStackDown
}
return nil
}
// compose executes a 'docker compose' command on this stack. // compose executes a 'docker compose' command on this stack.
// //
// NOTE(twiesing): Check if this can be replaced by an internal call to libcompose. // NOTE(twiesing): Check if this can be replaced by an internal call to libcompose.

View file

@ -34,8 +34,8 @@ type Instance struct {
// The filesystem path the system can be found under // The filesystem path the system can be found under
FilesystemBase string `gorm:"column:filesystem_base;not null"` FilesystemBase string `gorm:"column:filesystem_base;not null"`
// DockerBaseImage is the php base image to use // information about the system being used
DockerBaseImage string `gorm:"column:docker_base;not_null"` System `gorm:"embed"`
// SQL Database credentials for the system // SQL Database credentials for the system
SqlDatabase string `gorm:"column:sql_database;not null"` SqlDatabase string `gorm:"column:sql_database;not null"`
@ -48,38 +48,6 @@ type Instance struct {
GraphDBPassword string `gorm:"column:graphdb_password;not null"` GraphDBPassword string `gorm:"column:graphdb_password;not null"`
} }
// TODO: Cleanup this stuff
const (
PHP_DEFAULT_IMAGE = PHP8_1_IMAGE
PHP8 = "8.0"
PHP8_IMAGE = "docker.io/library/php:8.0-apache-bullseye"
PHP8_1 = "8.1"
PHP8_1_IMAGE = "docker.io/library/php:8.1-apache-bullseye"
)
var errUnknownPHPVersion = errors.New("unknown php version")
// GetBaseImage returns the php base image to use
func GetBaseImage(php string) (string, error) {
switch php {
case "":
return PHP_DEFAULT_IMAGE, nil
case PHP8:
return PHP8_IMAGE, nil
case PHP8_1:
return PHP8_1_IMAGE, nil
default:
return "", errUnknownPHPVersion
}
}
func (i Instance) GetDockerBaseImage() string {
if i.DockerBaseImage == "" {
return PHP8_IMAGE
}
return i.DockerBaseImage
}
func (i Instance) IsBlindUpdateEnabled() bool { func (i Instance) IsBlindUpdateEnabled() bool {
return bool(i.AutoBlindUpdateEnabled) return bool(i.AutoBlindUpdateEnabled)
} }

View file

@ -0,0 +1,50 @@
package models
// System represents system information.
// It is embedded into the instances struct by gorm.
type System struct {
// NOTE(twiesing): Any changes here should be reflected in instance_{provision,rebuild}.html and remote/api.ts.
PHP string `gorm:"column:php;not null"`
OpCacheDevelopment bool `gorm:"column:opcache_devel;not null"`
}
const (
imagePrefix = "docker.io/library/php:"
imageSuffix = "-apache-bullseye"
)
// OpCacheMode returns the name of the `opcache-*.ini` configuration being included in the docker image
func (system System) OpCacheMode() string {
if system.OpCacheDevelopment {
return "devel"
}
return "prod"
}
var (
phpVersions = []string{"8.0", "8.1", "8.2"}
phpVersionMap = (func() map[string]struct{} {
m := make(map[string]struct{}, len(phpVersions))
for _, v := range phpVersions {
m[v] = struct{}{}
}
return m
})()
)
// DefaultPHPVersion is the default php version
const DefaultPHPVersion = "8.1"
// KnownPHPVersions returns a slice of php versions.
func KnownPHPVersions() []string {
return append([]string(nil), phpVersions...)
}
// GetDockerBaseImage returns the docker base image used by the given system.
func (system System) GetDockerBaseImage() string {
version := DefaultPHPVersion
if _, ok := phpVersionMap[system.PHP]; ok {
version = system.PHP
}
return imagePrefix + version + imageSuffix
}

View file

@ -7,4 +7,6 @@ HOST_RULE=${HOST_RULE}
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME} DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
HTTPS_ENABLED=${HTTPS_ENABLED} HTTPS_ENABLED=${HTTPS_ENABLED}
BARREL_BASE_IMAGE=${BARREL_BASE_IMAGE} BARREL_BASE_IMAGE=${BARREL_BASE_IMAGE}
OPCACHE_MODE=${OPCACHE_MODE}

View file

@ -1,8 +1,6 @@
package barrel package barrel
import ( import (
"path/filepath"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore"
@ -17,6 +15,11 @@ type Barrel struct {
} }
} }
func (barrel *Barrel) DataPath() string { const (
return filepath.Join(barrel.FilesystemBase, "data") BaseDirectory = "/var/www/data"
} ComposerDirectory = BaseDirectory + "/project"
WebDirectory = ComposerDirectory + "/web"
OntologyDirectory = SitesDirectory + "/default/files/ontology"
SitesDirectory = WebDirectory + "/sites"
WissKIDirectory = WebDirectory + "/modules/contrib/wisski"
)

View file

@ -4,6 +4,5 @@
# allow the following files: # allow the following files:
!conf/* !conf/*
!scripts/* !scripts/*
!patch/*
!ssh/* !ssh/*
!wisskiutils/* !php.ini.d/*

View file

@ -1,6 +1,15 @@
ARG BARREL_BASE_IMAGE # ============================
# WissKI Distillery Dockerfile
# ============================
# This file is part of the WissKI Distillery and sets up an image
# to be used for individual WissKIs.
# Start from a base image (configured by the build argument).
ARG BARREL_BASE_IMAGE=docker.io/library/php:8.1-apache-bullseye
FROM $BARREL_BASE_IMAGE FROM $BARREL_BASE_IMAGE
ARG COMPOSER_VERSION=2.3.8
# Setup in /var/www
WORKDIR /var/www WORKDIR /var/www
# install and enable the various required php extensions and dropbear ssh server # install and enable the various required php extensions and dropbear ssh server
@ -69,39 +78,46 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
# enable the apache rewrite mod # enable the apache rewrite mod
RUN a2enmod rewrite RUN a2enmod rewrite
# install composer and add it to path
# Install composer.
ARG COMPOSER_VERSION=2.3.8
RUN curl -sS https://getcomposer.org/installer | php -- --version=$COMPOSER_VERSION && \ RUN curl -sS https://getcomposer.org/installer | php -- --version=$COMPOSER_VERSION && \
mv composer.phar /usr/local/bin/composer mv composer.phar /usr/local/bin/composer
# Add it to the path
ENV PATH "/usr/local/bin:/var/www/data/project/vendor/bin:$PATH" ENV PATH "/usr/local/bin:/var/www/data/project/vendor/bin:$PATH"
# remove default configuration # Configure PHP
ADD php.ini.d/wisski.ini /usr/local/etc/php/conf.d/wisski.ini
# Configure opcache with whatever the user configured
ARG OPCACHE_MODE=prod
ADD php.ini.d/opcache-$OPCACHE_MODE.ini /usr/local/etc/php/conf.d/opcache.ini
# Configure Apache.
# first remove the default configuration
RUN rm /etc/apache2/sites-available/*.conf && \ RUN rm /etc/apache2/sites-available/*.conf && \
rm /etc/apache2/sites-enabled/*.conf rm /etc/apache2/sites-enabled/*.conf
ADD patch/easyrdf.patch /patch/easyrdf.patch # Then add the WissKI site
ADD patch/triples.patch /patch/triples.patch
# Add wisski configuration
ADD conf/ports.conf /etc/apache2/ports.conf ADD conf/ports.conf /etc/apache2/ports.conf
ADD conf/wisski.conf /etc/apache2/sites-available/wisski.conf ADD conf/wisski.conf /etc/apache2/sites-available/wisski.conf
ADD conf/wisski.ini /usr/local/etc/php/conf.d/wisski.ini
# And enable it
RUN a2ensite wisski RUN a2ensite wisski
# volumes for composer # volumes for composer
VOLUME /var/www/.composer VOLUME /var/www/.composer
VOLUME /var/www/data VOLUME /var/www/data
# Add and configure the entrypoint # Add and configure the entrypoint
ADD scripts/entrypoint.sh /entrypoint.sh ADD scripts/entrypoint.sh /entrypoint.sh
ENTRYPOINT [ "/bin/bash", "/entrypoint.sh" ] ENTRYPOINT [ "/bin/bash", "/entrypoint.sh" ]
CMD ["apache2-foreground"] CMD ["apache2-foreground"]
# Add the provision script and WissKI utils
ADD scripts/provision_container.sh /provision_container.sh
ADD wisskiutils/ /wisskiutils
# Add the user_shell.sh # Add the user_shell.sh
ADD scripts/user_shell.sh /user_shell.sh ADD scripts/user_shell.sh /user_shell.sh
ADD ssh/ /ssh/ ADD ssh/ /ssh/

View file

@ -6,6 +6,9 @@ services:
context: . context: .
args: args:
BARREL_BASE_IMAGE: ${BARREL_BASE_IMAGE} BARREL_BASE_IMAGE: ${BARREL_BASE_IMAGE}
OPCACHE_MODE: ${OPCACHE_MODE}
logging:
driver: none
restart: always restart: always
hostname: ${WISSKI_HOSTNAME} hostname: ${WISSKI_HOSTNAME}
@ -32,11 +35,10 @@ services:
# volumes that are mounted # volumes that are mounted
volumes: volumes:
- ${DATA_PATH}/.composer:/var/www/.composer - ${DATA_PATH}/.composer:/var/www/.composer:rw
- ${DATA_PATH}/data:/var/www/data - ${DATA_PATH}/data:/var/www/data:rw
- ${DATA_PATH}/home:/var/www/ - ${DATA_PATH}/home:/var/www:rw
- ${DATA_PATH}/hostkeys:/ssh/hostkeys:rw - ${DATA_PATH}/hostkeys:/ssh/hostkeys:rw
- ${RUNTIME_DIR}:/runtime:ro
networks: networks:
default: default:

View file

@ -1,4 +0,0 @@
281c281
< if (preg_match("|^HTTP/([\d\.x]+) (\d+) ([^\r\n]+)|", $status, $m)) {
---
> if(preg_match("|^HTTP/([\d\.x]+) (\d+) ([^\r\n]*)|", $status, $m)) {

View file

@ -1,8 +0,0 @@
100c100
< if($result->o instanceof \EasyRdf_Resource) {
---
> if($result->o instanceof \EasyRdf\Resource) {
118c118
< $object_text = $result->o->getValue();
---
> $object_text = $result->o->dumpValue('string');

View file

@ -0,0 +1,6 @@
; ======== Distillery php.ini =============
; Opcache Development Settings
; =========================================
opcache.revalidate_freq=0
opcache.enable_cli=0

View file

@ -0,0 +1,9 @@
; ======== Distillery php.ini =============
; Opcache Production Settings
; =========================================
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.enable_cli=1

View file

@ -1,3 +1,8 @@
; ======== Distillery php.ini =============
; Main Configuration File (always included)
; =========================================
; File Uploads up to 1GB ; File Uploads up to 1GB
file_uploads = On file_uploads = On
upload_max_filesize = 1000M upload_max_filesize = 1000M

View file

@ -1,193 +0,0 @@
#!/bin/bash
set -e
function log_info() {
echo -e "\033[1m$1\033[0m"
}
function log_ok() {
echo -e "\033[0;32m$1\033[0m"
}
log_info " => Reading configuration variables"
INSTANCE_DOMAIN="$1"
echo "INSTANCE_DOMAIN=$INSTANCE_DOMAIN"
shift 1
MYSQL_DATABASE="$1"
echo "MYSQL_DATABASE=$MYSQL_DATABASE"
MYSQL_USER="$2"
echo "MYSQL_USER=$MYSQL_USER"
MYSQL_PASSWORD="$3"
echo "MYSQL_PASSWORD=$MYSQL_PASSWORD"
shift 3
GRAPHDB_REPO="$1"
echo "GRAPHDB_REPO=$GRAPHDB_REPO"
GRAPHDB_USER="$2"
echo "GRAPHDB_USER=$GRAPHDB_USER"
GRAPHDB_PASSWORD="$3"
echo "GRAPHDB_PASSWORD=$GRAPHDB_PASSWORD"
shift 3
GRAPHDB_HEADER="$(printf "%s:%s" "$GRAPHDB_USER" "$GRAPHDB_PASSWORD" | base64 -w 0)"
DRUPAL_USER="$1"
echo "DRUPAL_USER=$DRUPAL_USER"
DRUPAL_PASS="$2"
echo "DRUPAL_PASS=$DRUPAL_PASS"
shift 2
DRUPAL_VERSION="$1"
echo "DRUPAL_VERSION=$DRUPAL_VERSION"
shift 1
WISSKI_VERSION="$1"
echo "WISSKI_VERSION=$WISSKI_VERSION"
shift 1
log_info " => Preparing installation environment"
BASE_DIR="/var/www/data"
COMPOSER_DIR="$BASE_DIR/project"
WEB_DIR="$COMPOSER_DIR/web"
ONTOLOGY_DIR="$WEB_DIR/sites/default/files/ontology"
log_info " => Creating '$COMPOSER_DIR'"
mkdir -p "$COMPOSER_DIR"
cd "$COMPOSER_DIR"
# workaround for making the drupal sites directory writable
function drupal_sites_permission_workaround() {
chmod -R u+w "$WEB_DIR/sites/" || true
}
# install a module with composer and enable it with drush
# Example:
#
# composer_install_and_enable << EOF
# drupal/some_module:1.23 some_module
# drupal/other_module:2.34
# EOF
#
# Will install both modules, but only enable the first one.
function composer_install_and_enable() {
while IFS= read -r line; do
echo "$line" | (
read composer drush;
drupal_sites_permission_workaround
composer require "$composer"
if [ -n "$drush" ]; then
drush pm-enable --yes "$drush"
fi
)
done
}
function try_variants() {
for var in "$@"
do
if composer require --dry-run "$var" > /dev/null 2>&1; then
composer require "$var"
return 0;
fi
done
return 1;
}
# Create a new composer project.
log_info " => Creating composer project"
if [ -z "${DRUPAL_VERSION}" ]; then
composer --no-interaction create-project 'drupal/recommended-project:^9.0.0' .
else
composer --no-interaction create-project "drupal/recommended-project:$DRUPAL_VERSION" .
fi
# needed for composer > 2.2
composer --no-interaction config allow-plugins true
# Install drush so that we can automate a lot of things
log_info " => Installing 'drush'"
try_variants 'drush/drush' 'drush/drush:^12' 'drush/drush:^11' || (echo "No version of Drush is installable" && false)
# Use 'drush' to run the site-installation.
# Here we need to use the username, password and database creds we made above.
log_info " => Running drupal installation scripts"
drush site-install standard --yes --site-name=${INSTANCE_DOMAIN} \
--account-name=$DRUPAL_USER --account-pass=$DRUPAL_PASS \
--db-url=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@sql/${MYSQL_DATABASE}
drupal_sites_permission_workaround
# create a directory for ontologies.
log_info " => Creating '$ONTOLOGY_DIR'"
mkdir -p "$ONTOLOGY_DIR"
# Install some additional modules
# These neeed to go before WissKI because some are WissKI dependencies
log_info " => Installing and enabling modules"
composer_install_and_enable << EOF
drupal/inline_entity_form:^1.0@RC
drupal/imagemagick
drupal/image_effects
drupal/colorbox
drupal/devel:^4.1 devel
drupal/geofield:^1.40 geofield
drupal/geofield_map:^2.85 geofield_map
drupal/imce:^2.4 imce
drupal/remove_generator:^2.0 remove_generator
EOF
# Install the Wisski packages.
log_info " => Installing Wisski packages"
cd "$COMPOSER_DIR"
# install the development version when requested
if [ -z "${WISSKI_VERSION}" ]; then
composer require 'drupal/wisski'
else
composer require "drupal/wisski:$WISSKI_VERSION"
fi
# Install dependencies of WissKI
log_info " => Installing and patching Wisski dependencies"
pushd "$WEB_DIR/modules/contrib/wisski"
composer install
# Patch EasyRDF (for now)
EASYRDF_RESPONSE="./vendor/easyrdf/easyrdf/lib/EasyRdf/Http/Response.php"
if [ -f "$EASYRDF_RESPONSE" ]; then
patch -N "$EASYRDF_RESPONSE" < "/patch/easyrdf.patch"
fi
popd
log_info " => Enable Wisski modules"
drush pm-enable --yes wisski_core wisski_linkblock wisski_pathbuilder wisski_adapter_sparql11_pb wisski_salz
drupal_sites_permission_workaround
log_info " => Setting up WissKI Salz Adapter"
drush php:script /wisskiutils/create_adapter.php "$INSTANCE_DOMAIN" "$GRAPHDB_REPO" "$GRAPHDB_HEADER"
log_info " => Updating TRUSTED_HOST_PATTERNS in settings.php"
/bin/bash /wisskiutils/set_trusted_host.sh
log_info " => Running initial cron"
drush core-cron
log_info " => Provisioning is now complete. "
log_ok "Your installation details are as follows:"
function printdetails() {
echo "URL: http://$INSTANCE_DOMAIN"
echo "Username: $DRUPAL_USER"
echo "Password: $DRUPAL_PASS"
}
printdetails
exit 0

View file

@ -1,61 +0,0 @@
<?php
/**
* This script will automatically create a WissKI Salz Adapter for use within the distillery.
* It will not update any existing adapter and is rather primitive.
*/
$argc = $_SERVER['argc']-3;
$argv = array_slice($_SERVER['argv'], 3);
// read parameters from the command line
if ($argc != 3) {
die("Usage: drush php:script create_adapter.php INSTANCE_DOMAIN GRAPHDB_REPO HEADER");
}
$INSTANCE_DOMAIN = $argv[0];
$GRAPHDB_REPO = $argv[1];
$HEADER = $argv[2];
//
// PROPERTIES FOR THE ADAPTER
//
$id = 'default'; // id
$type = 'sparql11_with_pb'; // plugin
$machine_name = 'default'; // machine-name
$label = 'Default WissKI Distillery Adapter';
$description = 'Adapter for ' . $INSTANCE_DOMAIN; // description
$writable = TRUE; // writable
$is_preferred_local_store = TRUE; // is_preferred_local_store
$header = $HEADER; // header
$read_url = 'http://triplestore:7200/repositories/' . $GRAPHDB_REPO; // read_url
$write_url = 'http://triplestore:7200/repositories/' . $GRAPHDB_REPO . '/statements'; // write_url
$is_federatable = TRUE; // is_federatable
$default_graph_uri = 'https://' . $INSTANCE_DOMAIN . '/';
$same_as_properties = ['http://www.w3.org/2002/07/owl#sameAs']; // same_as_properties
$ontology_graphs = []; // ontology_graphs
//
// Do the creation!
//
$storage = \Drupal::entityTypeManager()->getStorage('wisski_salz_adapter');
$adapter = $storage->create([
"id" => $id,
"label" => $label,
"description" => $description,
]);
$adapter->setEngineConfig([
"id" => $type,
"machine-name" => $machine_name,
"header" => $header,
"writeable" => $writable,
"is_preferred_local_store" => $is_preferred_local_store,
"read_url" => $read_url,
"write_url" => $write_url,
"is_federatable" => $is_federatable,
"default_graph" => $default_graph_uri,
"same_as_properties" => $same_as_properties,
"ontology_graphs" => $ontology_graphs,
]);
$adapter->save();

View file

@ -1,13 +0,0 @@
#!/bin/bash
# This utility script can be used to configure the trusted host settings inside of settings.php.
# It doesn't take care of corner cases and should only be used when needed.
INSTANCE_DOMAIN="$(hostname -f)"
INSTANCE_DOMAIN="${INSTANCE_DOMAIN%.wisski}"
TRUSTED_HOST_PATTERN="${INSTANCE_DOMAIN//\./\\\\.}"
TRUSTED_HOST_PATTERNS='["'$TRUSTED_HOST_PATTERN'"]'
echo "Setting 'trusted_host_patterns' to $TRUSTED_HOST_PATTERNS"
bash /wisskiutils/settings_php_set.sh 'trusted_host_patterns' "$TRUSTED_HOST_PATTERNS"

View file

@ -1,17 +0,0 @@
#!/bin/bash
# settings_php_get.sh name
# Gets the 'settings_php_get.php' setting 'name' as json-encoded value, or null when it does not exist.
NAME=$1
if [ -z "$NAME" ]; then
echo "Usage: get_settings_setting.sh NAME"
exit 1
fi;
echo "$NAME" | drush php:eval '
use \Drupal\Core\Site\Settings;
$name=trim(file_get_contents("php://stdin"));
echo json_encode(Settings::get($name));
';

View file

@ -1,60 +0,0 @@
#!/bin/bash
# settings_php_set.sh name value
# Sets the 'settings.php' setting 'name' to 'value'.
# Value must be json-encoded.
NAME=$1
VALUE=$2
if [ -z "$NAME" ]; then
echo "Usage: settings_php_set.sh NAME VALUE"
exit 1
fi;
if [ -z "$VALUE" ]; then
echo "Usage: settings_php_set.sh NAME VALUE"
exit 1
fi;
cd /var/www/data/project
chmod u+w web/sites/default/settings.php
(echo "$NAME"; echo "$VALUE" ) | drush php:eval '
if(is_file(DRUPAL_ROOT . "/internal/")) {
include_once DRUPAL_ROOT . "/internal/core/includes/install.inc";
} else {
include_once DRUPAL_ROOT . "/core/includes/install.inc";
}
// read NAME and VALUE from STDIN
$content=file_get_contents("php://stdin");
$newline=strpos($content, "\n");
$name=trim(substr($content, 0, $newline));
$jvalue=trim(substr($content, $newline + 1));
// decode json values
$value = @json_decode($jvalue);
if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
echo "Invalid JSON, cannot update settings.php. \n";
return 1;
}
// make parameters to drush_rewrite_settings
$settings["settings"][$name] = (object)[
"value" => $value,
"required" => TRUE,
];
// find the actual settings.php file to rewrite
$filename = DRUPAL_ROOT . "/" . \Drupal::service("site.path") . "/settings.php";
drupal_rewrite_settings($settings, $filename);
echo "Wrote " . $filename . "\n";
return 0;
';
EXIT=$?
chmod u-w web/sites/default/settings.php
exit $?

View file

@ -0,0 +1,59 @@
package composer
import (
"context"
"errors"
"io"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/tkw1536/pkglib/stream"
)
// Drush implements commands related to drush
type Composer struct {
ingredient.Base
Dependencies struct {
Barrel *barrel.Barrel
// PHP *php.PHP
}
}
// Exec executes a composer command
func (composer *Composer) Exec(ctx context.Context, progress io.Writer, command ...string) error {
if err := composer.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), append([]string{"composer", "--no-interaction", "--working-dir", barrel.ComposerDirectory}, command...)...); err != nil {
return err
}
return nil
}
// FixPermissions fixes the permissions of the sites directory.
// This needs to be run after every installation of a composer module.
func (composer *Composer) FixPermission(ctx context.Context, progress io.Writer) error {
composer.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), "chmod", "-R", "u+w", barrel.SitesDirectory)
return nil
}
// Install attempts runs 'composer require' with the given arguments
// Spec is like a specification on the command line.
func (composer *Composer) Install(ctx context.Context, progress io.Writer, args ...string) error {
composer.FixPermission(ctx, progress)
requires := append([]string{"require"}, args...)
if err := composer.Exec(ctx, progress, requires...); err != nil {
return err
}
return nil
}
var ErrNotInstalled = errors.New("Composer: Not installed")
// TryInstall attempts to install the given package.
// If it cannot be installed, returns ErrNotInstalled.
func (composer *Composer) TryInstall(ctx context.Context, progress io.Writer, spec string) error {
if err := composer.Exec(ctx, io.Discard, "require", "--dry-run", spec); err != nil {
return ErrNotInstalled
}
return composer.Install(ctx, progress, spec)
}

View file

@ -0,0 +1,14 @@
package composer
import "strings"
// ModuleName extracts the module name from a specification.
// If the module name cannot be found, returns the string unchanged
func ModuleName(spec string) string {
_, name, found := strings.Cut(spec, "/")
if !found {
return spec
}
name, _, _ = strings.Cut(name, ":")
return name
}

View file

@ -10,8 +10,8 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/phpx"
"github.com/FAU-CDI/wisski-distillery/internal/status" "github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/pkglib/stream"
) )
var errCronFailed = exit.Error{ var errCronFailed = exit.Error{
@ -20,8 +20,9 @@ var errCronFailed = exit.Error{
} }
func (drush *Drush) Cron(ctx context.Context, progress io.Writer) error { func (drush *Drush) Cron(ctx context.Context, progress io.Writer) error {
code := drush.Dependencies.Barrel.Shell(ctx, stream.NonInteractive(progress), "/runtime/cron.sh")() err := drush.Exec(ctx, progress, "core-cron")
if code != 0 { if err != nil {
code := err.(barrel.ExitError).Code
// keep going, because we want to run as many crons as possible // keep going, because we want to run as many crons as possible
fmt.Fprintf(progress, "%v", errCronFailed.WithMessageF(drush.Slug, code)) fmt.Fprintf(progress, "%v", errCronFailed.WithMessageF(drush.Slug, code))
} }
@ -31,7 +32,7 @@ func (drush *Drush) Cron(ctx context.Context, progress io.Writer) error {
func (drush *Drush) LastCron(ctx context.Context, server *phpx.Server) (t time.Time, err error) { func (drush *Drush) LastCron(ctx context.Context, server *phpx.Server) (t time.Time, err error) {
var timestamp int64 var timestamp int64
err = drush.Dependencies.PHP.EvalCode(ctx, server, &timestamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `) err = drush.Dependencies.PHP.EvalCode(ctx, server, &timestamp, `return \Drupal::state()->get('system.cron_last');`)
if err != nil { if err != nil {
return return
} }

View file

@ -1,10 +1,14 @@
package drush package drush
import ( import (
"context"
"io"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
"github.com/tkw1536/pkglib/stream"
) )
// Drush implements commands related to drush // Drush implements commands related to drush
@ -16,3 +20,13 @@ type Drush struct {
PHP *php.PHP PHP *php.PHP
} }
} }
// Enable enables the given drush modules
func (drush *Drush) Enable(ctx context.Context, progress io.Writer, modules ...string) error {
return drush.Exec(ctx, progress, append([]string{"pm-enable", "--yes"}, modules...)...)
}
func (drush *Drush) Exec(ctx context.Context, progress io.Writer, command ...string) error {
script := append([]string{"drush"}, command...)
return drush.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), script...)
}

View file

@ -14,15 +14,15 @@ import (
) )
var errBlindUpdateFailed = exit.Error{ var errBlindUpdateFailed = exit.Error{
Message: "failed to run blind update script for instance %q: exited with code %d", Message: "failed to run blind update script for instance %q",
ExitCode: exit.ExitGeneric, ExitCode: exit.ExitGeneric,
} }
// Update performs a blind drush update // Update performs a blind drush update
func (drush *Drush) Update(ctx context.Context, progress io.Writer) error { func (drush *Drush) Update(ctx context.Context, progress io.Writer) error {
code := drush.Dependencies.Barrel.Shell(ctx, stream.NonInteractive(progress), "/runtime/blind_update.sh")() err := drush.Dependencies.Barrel.Shell(ctx, stream.NonInteractive(progress), "/runtime/blind_update.sh")
if code != 0 { if err != nil {
return errBlindUpdateFailed.WithMessageF(drush.Slug, code) return errBlindUpdateFailed.WithMessageF(drush.Slug).Wrap(err)
} }
return drush.setLastUpdate(ctx) return drush.setLastUpdate(ctx)

View file

@ -0,0 +1,196 @@
package manager
import (
"context"
"fmt"
"io"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/composer"
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
"github.com/tkw1536/pkglib/stream"
)
// Apply applies the given profile to this existing instance.
func (manager *Manager) Apply(ctx context.Context, progress io.Writer, flags Profile) error {
// Update drupal
if flags.Drupal != "" {
err := manager.applyDrupal(ctx, progress, flags.Drupal)
if err != nil {
return err
}
}
// Update WissKI
if flags.WissKI != "" {
err := manager.applyWissKI(ctx, progress, flags.WissKI)
if err != nil {
return err
}
}
// install custom modules
if len(flags.InstallModules) > 0 {
err := manager.installModules(ctx, progress, flags.InstallModules, false)
if err != nil {
return err
}
}
// install + enable custom modules
if len(flags.EnableModules) > 0 {
err := manager.installModules(ctx, progress, flags.EnableModules, true)
if err != nil {
return err
}
}
return nil
}
func (manager *Manager) installModules(ctx context.Context, progress io.Writer, modules []string, enable bool) error {
message := ""
if enable {
message = "Installing and enabling modules"
} else {
message = "Installing modules"
}
// enable the module
return logging.LogOperation(func() error {
for _, spec := range modules {
logging.LogMessage(progress, fmt.Sprintf("Installing %q", spec))
err := manager.Dependencies.Composer.Install(ctx, progress, spec)
if err != nil {
return err
}
if enable {
name := composer.ModuleName(spec)
logging.LogMessage(progress, fmt.Sprintf("Enabling %q (from spec %q)", name, spec))
err := manager.Dependencies.Drush.Enable(ctx, progress, name)
if err != nil {
return err
}
}
}
return nil
}, progress, "%s", message)
}
// applyDrupal applies a specific drupal version.
// Assumes that drupal != "".
func (manager *Manager) applyDrupal(ctx context.Context, progress io.Writer, drupal string) error {
return logging.LogOperation(func() error {
logging.LogMessage(progress, "Clearing up permissions for update")
{
for _, script := range [][]string{
{"chmod", "777", "web/sites/default"},
{"chmod", "666", "web/sites/default/*settings.php"},
{"chmod", "666", "web/sites/default/*services.php"},
} {
err := manager.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), script...)
if err != nil {
return err
}
}
}
defer func() {
logging.LogMessage(progress, "Resetting permissions")
{
for _, script := range [][]string{
{"chmod", "755", "web/sites/default"},
{"chmod", "644", "web/sites/default/*settings.php"},
{"chmod", "644", "web/sites/default/*services.php"},
} {
manager.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), script...)
}
}
}()
// write out a specific Drupal version
logging.LogMessage(progress, "Performing Drupal update")
{
args := []string{
"drupal/internal/core-recommended:", "drupal/internal/core-composer-scaffold:", "drupal/internal/core-project-message:",
}
for i, cm := range args {
args[i] = cm + drupal
}
args = append(args, "--update-with-dependencies", "--no-update")
if err := manager.Dependencies.Composer.Install(ctx, progress, args...); err != nil {
return err
}
}
logging.LogMessage(progress, "Running composer update")
{
if err := manager.Dependencies.Composer.Exec(ctx, progress, "update"); err != nil {
return err
}
}
logging.LogMessage(progress, "Performing database updates (if any)")
{
if err := manager.Dependencies.Drush.Exec(ctx, progress, "updatedb", "--yes"); err != nil {
return err
}
}
return nil
}, progress, "%s", "Updating to Drupal %q", drupal)
}
// applyWissKI applies the WissKI version.
func (manager *Manager) applyWissKI(ctx context.Context, progress io.Writer, wisski string) error {
return logging.LogOperation(func() error {
logging.LogMessage(progress, "Installing WissKI Module")
{
spec := "drupal/wisski"
if wisski != "" {
spec += ":" + wisski
}
err := manager.Dependencies.Composer.Install(ctx, progress, spec)
if err != nil {
return err
}
}
// install dependencies in the WissKI directory
logging.LogMessage(progress, "Installing WissKI Dependencies")
{
if err := manager.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), "composer", "--working-dir", barrel.WissKIDirectory, "install"); err != nil {
return err
}
}
logging.LogMessage(progress, "Enable Wisski modules")
{
if err := manager.Dependencies.Drush.Enable(ctx, progress,
"wisski_core", "wisski_linkblock", "wisski_pathbuilder", "wisski_adapter_sparql11_pb", "wisski_salz",
); err != nil {
return err
}
if err := manager.Dependencies.Composer.FixPermission(ctx, progress); err != nil {
return err
}
}
logging.LogMessage(progress, "Performing database updates (if any)")
{
if err := manager.Dependencies.Drush.Exec(ctx, progress, "updatedb", "--yes"); err != nil {
return err
}
}
return nil
}, progress, "Installing WissKI version %q", wisski)
}

View file

@ -0,0 +1,64 @@
package manager
import (
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/composer"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/drush"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/system"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/bookkeeping"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/extras"
)
// Manager manages a profile applied to specific WissKI instances.
type Manager struct {
ingredient.Base
Dependencies struct {
Barrel *barrel.Barrel
Bookkeeping *bookkeeping.Bookkeeping
SystemManager *system.SystemManager
Composer *composer.Composer
Drush *drush.Drush
Adapters *extras.Adapters
Settings *extras.Settings
}
}
// Profile represents a profile applied to a WissKI instance of the Distillery.
type Profile struct {
Drupal string // Version of Drupal to use
WissKI string // Version of WissKI to use
InstallModules []string // Modules to be installed (but not neccessarily enabled)
EnableModules []string // Modules to be installed and enabled
}
// DefaultDrupalVersion is the default drupal version
const DefaultDrupalVersion = "^9.0.0"
// ApplyDefaults applies the default settings to missing profile settings.
func (profile *Profile) ApplyDefaults() {
if profile.Drupal == "" {
profile.Drupal = DefaultDrupalVersion
}
if profile.InstallModules == nil {
profile.InstallModules = []string{
"drupal/inline_entity_form:^1.0@RC",
"drupal/imagemagick",
"drupal/image_effects",
"drupal/colorbox",
}
}
if profile.EnableModules == nil {
profile.EnableModules = []string{
"drupal/devel:^4.1",
"drupal/geofield:^1.40",
"drupal/geofield_map:^2.85",
"drupal/imce:^2.4",
"drupal/remove_generator:^2.0",
}
}
}

View file

@ -0,0 +1,196 @@
package manager
import (
"context"
"fmt"
"io"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/composer"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/extras"
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
"github.com/tkw1536/pkglib/contextx"
"github.com/tkw1536/pkglib/stream"
)
// Provision provisions this instance with the given flags.
//
// Provision assumes that the instance does not yet exist, and may fail with an existing instance.
//
// Provision applies defaults to flags, to ensure some values are set
func (manager *Manager) Provision(ctx context.Context, progress io.Writer, system models.System, flags Profile) error {
// Force building and applying the system!
if err := manager.Dependencies.SystemManager.Apply(ctx, progress, system, false); err != nil {
return err
}
// Create the composer directory!
logging.LogMessage(progress, "Creating required directories")
{
code, err := manager.Dependencies.Barrel.Stack().Run(ctx, stream.FromNil(), component.RunFlags{Detach: true, AutoRemove: true}, "barrel", "sudo", "-u", "www-data", "mkdir", "-p", barrel.ComposerDirectory)
if code != 0 {
err = barrel.ExitError(code)
}
if err != nil {
return err
}
}
// start the container, and have it do nothing!
code, err := manager.Dependencies.Barrel.Stack().Run(ctx, stream.FromNil(), component.RunFlags{Detach: true, AutoRemove: true}, "barrel", "tail", "-f", "/dev/null")
if code != 0 {
err = barrel.ExitError(code)
}
if err != nil {
return err
}
// when we are done, shut it down!
defer func() {
anyways, cancel := contextx.Anyways(ctx, time.Minute)
defer cancel()
// stop the container (even if the context was cancelled)
manager.Dependencies.Barrel.Stack().DownAll(anyways, progress)
}()
// Apply the defaults to the flags
flags.ApplyDefaults()
return manager.bootstrap(ctx, progress, flags)
}
// TODO: Move this to the flags
var drushVariants = []string{
"drush/drush", "drush/drush:^12", "drush/drush:^11",
}
// bootstrap applies the initial flags induced by flags.
// Applies defaults to the flags.
func (provision *Manager) bootstrap(ctx context.Context, progress io.Writer, flags Profile) error {
// TODO: Check if we can remove the easyrdf patch!
flags.ApplyDefaults()
logging.LogMessage(progress, "Creating Composer Project")
{
drupal := "drupal/recommended-project"
if flags.Drupal != "" {
drupal += ":" + flags.Drupal
}
err := provision.Dependencies.Composer.Exec(ctx, progress, "create-project", drupal, ".")
if err != nil {
return err
}
}
logging.LogMessage(progress, "Configuring Composer")
{
// needed for composer > 2.2
err := provision.Dependencies.Composer.Exec(ctx, progress, "config", "allow-plugins", "true")
if err != nil {
return err
}
}
logging.LogMessage(progress, "Installing drush")
{
for _, v := range drushVariants {
err := provision.Dependencies.Composer.TryInstall(ctx, progress, v)
if err == composer.ErrNotInstalled {
continue
}
if err != nil {
return err
}
break
}
}
var sqlDBURL = "mysql://" + provision.SqlUsername + ":" + provision.SqlPassword + "@sql/" + provision.SqlDatabase
// Use 'drush' to run the site-installation.
// Here we need to use the username, password and database creds we made above.
logging.LogMessage(progress, "Running Drupal installation scripts")
{
if err := provision.Dependencies.Drush.Exec(
ctx, progress,
"site-install",
"standard", "--yes", "--site-name="+provision.Domain(),
"--account-name="+provision.DrupalUsername, "--account-pass="+provision.DrupalPassword,
"--db-url="+sqlDBURL,
); err != nil {
return err
}
if err := provision.Dependencies.Composer.FixPermission(ctx, progress); err != nil {
return err
}
}
// Create directory for ontologies
logging.LogMessage(progress, fmt.Sprintf("Creating %q", barrel.OntologyDirectory))
{
if err := provision.Dependencies.Barrel.ShellScript(ctx, stream.NonInteractive(progress), "mkdir", "-p", barrel.OntologyDirectory); err != nil {
return err
}
}
{
// make a set of flags to apply to the given instance
flags := flags
flags.Drupal = "" // Do not upgrade Drupal
flags.WissKI = "" // Do not upgrade WissKI
// apply the rest of the flags
if err := provision.Apply(ctx, progress, flags); err != nil {
return err
}
}
// install WissKI
if err := provision.applyWissKI(ctx, progress, flags.WissKI); err != nil {
return err
}
// create the default adapter
logging.LogMessage(progress, "Creating default adapter")
{
if err := provision.Dependencies.Adapters.CreateDistilleryAdapter(ctx, nil, extras.DistilleryAdapter{
Label: "Default WissKI Distillery Adapter",
MachineName: "default",
Description: "Default Adapter for " + provision.Domain(),
InstanceDomain: provision.Domain(),
GraphDBRepository: provision.GraphDBRepository,
GraphDBUsername: provision.GraphDBUsername,
GraphDBPassword: provision.GraphDBPassword,
}); err != nil {
return err
}
}
logging.LogMessage(progress, "Updating TRUSTED_HOST_PATTERNS in settings.php")
{
if err := provision.Dependencies.Settings.SetTrustedDomain(ctx, nil, provision.Domain()); err != nil {
return err
}
}
logging.LogMessage(progress, "Running initial cron")
{
if err := provision.Dependencies.Drush.Exec(ctx, progress, "core-cron"); err != nil {
return err
}
}
logging.LogMessage(progress, "Provisioning is now complete")
{
fmt.Fprintf(progress, "URL: %s\n", provision.URL())
fmt.Fprintf(progress, "Username: %s\n", provision.DrupalUsername)
fmt.Fprintf(progress, "Password: %s\n", provision.DrupalPassword)
}
return nil
}

View file

@ -1,88 +0,0 @@
package provisioner
import (
"context"
"errors"
"io"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/bookkeeping"
"github.com/alessio/shellescape"
"github.com/tkw1536/pkglib/stream"
)
// Provisioner provides provisioning for a barrel
// NOTE(twiesing): This should be refactored to not use the provision script.
// Instead, this should code directly defined in go.
type Provisioner struct {
ingredient.Base
Dependencies struct {
Barrel *barrel.Barrel
Bookkeeping *bookkeeping.Bookkeeping
}
}
// ApplyFlags applies flags to an already provisioned instance.
func (provision *Provisioner) ApplyFlags(ctx context.Context, progress io.Writer, phpversion string) (err error) {
// setup the new docker image
provision.Instance.DockerBaseImage, err = models.GetBaseImage(phpversion)
if err != nil {
return err
}
// save in bookkeeping
if err := provision.Dependencies.Bookkeeping.Save(ctx); err != nil {
return err
}
return provision.Dependencies.Barrel.Build(ctx, progress, true)
}
// Provision provisions an instance, assuming that the required databases already exist.
func (provision *Provisioner) Provision(ctx context.Context, progress io.Writer) error {
// build the container
if err := provision.Dependencies.Barrel.Build(ctx, progress, false); err != nil {
return err
}
provisionParams := []string{
provision.Domain(),
provision.SqlDatabase,
provision.SqlUsername,
provision.SqlPassword,
provision.GraphDBRepository,
provision.GraphDBUsername,
provision.GraphDBPassword,
provision.DrupalUsername,
provision.DrupalPassword,
"", // TODO: DrupalVersion
"", // TODO: WissKIVersion
}
// escape the parameter
for i, param := range provisionParams {
provisionParams[i] = shellescape.Quote(param)
}
// figure out the provision script
// 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 := provision.Dependencies.Barrel.Stack().Run(ctx, stream.NonInteractive(progress), true, "barrel", "/bin/bash", "-c", provisionScript)
if err != nil {
return err
}
if code != 0 {
return errors.New("unable to run provision script")
}
return nil
}

View file

@ -2,11 +2,35 @@ package barrel
import ( import (
"context" "context"
"fmt"
"github.com/alessio/shellescape"
"github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/pkglib/stream" "github.com/tkw1536/pkglib/stream"
) )
// Shell executes a shell command inside the instance. type ExitError int
func (barrel *Barrel) Shell(ctx context.Context, io stream.IOStream, argv ...string) func() int {
return barrel.Stack().Exec(ctx, io, "barrel", "/bin/sh", append([]string{"/user_shell.sh"}, argv...)...) func (ee ExitError) Error() string {
return fmt.Sprintf("Exited with code %d", int(ee))
}
func (ee ExitError) Code() exit.ExitCode {
return exit.ExitCode(ee)
}
// Shell executes a shell with the given command line arguments inside the container.
// If an error occurs, it is of type ExitError.
func (barrel *Barrel) Shell(ctx context.Context, io stream.IOStream, argv ...string) error {
code := barrel.Stack().Exec(ctx, io, "barrel", "/bin/sh", append([]string{"/user_shell.sh"}, argv...)...)()
if code != 0 {
return ExitError(code)
}
return nil
}
// ShellScript quotes the given command and executes it as a shell script inside the container.
func (barrel *Barrel) ShellScript(ctx context.Context, io stream.IOStream, commands ...string) error {
command := shellescape.QuoteCommand(commands)
return barrel.Shell(ctx, io, "-c", command)
} }

View file

@ -33,6 +33,7 @@ func (barrel *Barrel) Stack() component.StackWithResources {
"RUNTIME_DIR": barrel.Malt.Config.Paths.RuntimeDir(), "RUNTIME_DIR": barrel.Malt.Config.Paths.RuntimeDir(),
"BARREL_BASE_IMAGE": barrel.GetDockerBaseImage(), "BARREL_BASE_IMAGE": barrel.GetDockerBaseImage(),
"OPCACHE_MODE": barrel.OpCacheMode(),
}, },
MakeDirs: []string{"data", ".composer"}, MakeDirs: []string{"data", ".composer"},

View file

@ -0,0 +1,37 @@
package system
import (
"context"
"io"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/bookkeeping"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/extras"
)
// SystemManager applies a specific system configuration
type SystemManager struct {
ingredient.Base
Dependencies struct {
Barrel *barrel.Barrel
Bookkeeping *bookkeeping.Bookkeeping
Settings *extras.Settings
}
}
// Apply applies a specific system version to this barrel.
// If start is true, also starts the container.
func (smanager *SystemManager) Apply(ctx context.Context, progress io.Writer, system models.System, start bool) (err error) {
// setup the new docker image
smanager.Instance.System = system
// save in bookkeeping
if err := smanager.Dependencies.Bookkeeping.Save(ctx); err != nil {
return err
}
// and rebuild
return smanager.Dependencies.Barrel.Build(ctx, progress, start)
}

View file

@ -0,0 +1,41 @@
package extras
import (
"context"
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
_ "embed"
)
// Prefixes implements reading and writing prefix
type Adapters struct {
ingredient.Base
Dependencies struct {
PHP *php.PHP
}
}
//go:embed adapters.php
var adaptersPHP string
type DistilleryAdapter struct {
Label string
MachineName string
Description string
InstanceDomain string
GraphDBRepository string
GraphDBUsername string
GraphDBPassword string
}
func (wisski *Adapters) CreateDistilleryAdapter(ctx context.Context, server *phpx.Server, adapter DistilleryAdapter) error {
return wisski.Dependencies.PHP.ExecScript(
ctx, server, nil, adaptersPHP,
"create_distillery_adapter",
adapter.Label, adapter.MachineName, adapter.Description, adapter.InstanceDomain, adapter.GraphDBRepository, adapter.GraphDBUsername, adapter.GraphDBPassword,
)
}

View file

@ -0,0 +1,57 @@
<?php
/**
* Creates an adapter for the distillery
*/
function create_distillery_adapter(string $LABEL, string $MACHINE_NAME, string $DESCRIPTION, string $INSTANCE_DOMAIN, string $GRAPHDB_REPO, string $GRAPHDB_USER, string $GRAPHDB_PASSWORD) {
//
// PROPERTIES FOR THE ADAPTER
//
$id = 'default'; // id
$type = 'sparql11_with_pb'; // plugin
$machine_name = $MACHINE_NAME; // machine-name
$label = $LABEL;
$description = $DESCRIPTION; // description
$writable = TRUE; // writable
$is_preferred_local_store = TRUE; // is_preferred_local_store
$read_url = 'http://triplestore:7200/repositories/' . $GRAPHDB_REPO; // read_url
$write_url = 'http://triplestore:7200/repositories/' . $GRAPHDB_REPO . '/statements'; // write_url
$is_federatable = TRUE; // is_federatable
$default_graph_uri = 'https://' . $INSTANCE_DOMAIN . '/';
$same_as_properties = ['http://www.w3.org/2002/07/owl#sameAs']; // same_as_properties
$ontology_graphs = []; // ontology_graphs
// header
$header = "";
if ($GRAPHDB_USER !== "" && $GRAPHDB_PASSWORD !== "") {
$header = $GRAPHDB_USER . ":" . $GRAPHDB_PASSWORD;
$header = base64_encode($header);
}
//
// Do the creation!
//
$storage = \Drupal::entityTypeManager()->getStorage('wisski_salz_adapter');
$adapter = $storage->create([
"id" => $id,
"label" => $label,
"description" => $description,
]);
$adapter->setEngineConfig([
"id" => $type,
"machine-name" => $machine_name,
"header" => $header,
"writeable" => $writable,
"is_preferred_local_store" => $is_preferred_local_store,
"read_url" => $read_url,
"write_url" => $write_url,
"is_federatable" => $is_federatable,
"default_graph" => $default_graph_uri,
"same_as_properties" => $same_as_properties,
"ontology_graphs" => $ontology_graphs,
]);
$adapter->save();
}

View file

@ -3,6 +3,7 @@ package extras
import ( import (
"context" "context"
_ "embed" _ "embed"
"errors"
"github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/phpx"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
@ -24,6 +25,25 @@ func (settings *Settings) Get(ctx context.Context, server *phpx.Server, key stri
return return
} }
var errFailedToSetSetting = errors.New("failed to update setting")
func (settings *Settings) Set(ctx context.Context, server *phpx.Server, key string, value any) error { func (settings *Settings) Set(ctx context.Context, server *phpx.Server, key string, value any) error {
return settings.Dependencies.PHP.ExecScript(ctx, server, nil, settingsPHP, "set_setting", key, value) var ok bool
err := settings.Dependencies.PHP.ExecScript(ctx, server, &ok, settingsPHP, "set_setting", key, value)
if err == nil && !ok {
err = errFailedToSetSetting
}
return err
}
var errFailedToSetTrustedDomain = errors.New("failed to set trusted domain")
func (settings *Settings) SetTrustedDomain(ctx context.Context, server *phpx.Server, domain string) error {
var ok bool
err := settings.Dependencies.PHP.ExecScript(ctx, server, &ok, settingsPHP, "set_trusted_domain", domain)
if err == nil && !ok {
err = errFailedToSetTrustedDomain
}
return err
} }

View file

@ -1,13 +1,28 @@
<?php <?php
use \Drupal\Core\Site\Settings;
/** gets a setting from 'settings.php' */ /** gets a setting from 'settings.php' */
function get_setting($name) { function get_setting($name) {
use \Drupal\Core\Site\Settings;
return Settings::get($name); return Settings::get($name);
} }
/** sets a setting in 'settings.php' */ /** sets a setting in 'settings.php' */
function set_setting($name, $value) { function set_setting(string $name, mixed $value): bool {
// find settings.php
$filename = DRUPAL_ROOT . "/" . \Drupal::service("site.path") . "/settings.php";
// setup user write permissions for the file
$old = fileperms($filename);
if ($old === FALSE) {
return FALSE;
}
$new = 0777; // set all permissions
if (!chmod($filename, $new)) {
return FALSE;
}
// load install.inc // load install.inc
if(is_file(DRUPAL_ROOT . "/internal/")) { if(is_file(DRUPAL_ROOT . "/internal/")) {
include_once DRUPAL_ROOT . "/internal/core/includes/install.inc"; include_once DRUPAL_ROOT . "/internal/core/includes/install.inc";
@ -21,9 +36,20 @@ function set_setting($name, $value) {
"required" => TRUE, "required" => TRUE,
]; ];
// find the filename // do the rewrite
$filename = DRUPAL_ROOT . "/" . \Drupal::service("site.path") . "/settings.php"; try {
drupal_rewrite_settings($settings, $filename); drupal_rewrite_settings($settings, $filename);
} catch(Throwable $t) {
return True; throw $t; // DEBUG
return FALSE;
}
// reset the file mode
return chmod($filename, $old);
}
/** Sets the trusted host to the specified domain */
function set_trusted_domain(string $domain): bool {
return set_setting("trusted_host_patterns", [preg_quote($domain)]);
} }

View file

@ -5,7 +5,6 @@ import (
_ "embed" _ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/phpx"
"github.com/alessio/shellescape"
"github.com/tkw1536/pkglib/stream" "github.com/tkw1536/pkglib/stream"
) )
@ -21,6 +20,6 @@ func (php *PHP) NewServer() *phpx.Server {
} }
func (php *PHP) spawn(ctx context.Context, str stream.IOStream, code string) error { func (php *PHP) spawn(ctx context.Context, str stream.IOStream, code string) error {
php.Dependencies.Barrel.Shell(ctx, str, "-c", shellescape.QuoteCommand([]string{"drush", "php:eval", code}))() php.Dependencies.Barrel.ShellScript(ctx, str, "drush", "php:eval", code)
return nil return nil
} }

View file

@ -6,9 +6,11 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/composer"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/drush" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/drush"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/provisioner" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/manager"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/ssh" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/ssh"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel/system"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/bookkeeping" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/bookkeeping"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/info" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/info"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker"
@ -46,8 +48,12 @@ func (wisski *WissKI) Barrel() *barrel.Barrel {
return export[*barrel.Barrel](wisski) return export[*barrel.Barrel](wisski)
} }
func (wisski *WissKI) Provisioner() *provisioner.Provisioner { func (wisski *WissKI) Manager() *manager.Manager {
return export[*provisioner.Provisioner](wisski) return export[*manager.Manager](wisski)
}
func (wisski *WissKI) SystemManager() *system.SystemManager {
return export[*system.SystemManager](wisski)
} }
func (wisski *WissKI) PHP() *php.PHP { func (wisski *WissKI) PHP() *php.PHP {
@ -111,6 +117,7 @@ func (wisski *WissKI) allIngredients() []initFunc {
auto[*extras.Stats], auto[*extras.Stats],
auto[*extras.Blocks], auto[*extras.Blocks],
auto[*extras.Requirements], auto[*extras.Requirements],
auto[*extras.Adapters],
auto[*users.Users], auto[*users.Users],
auto[*users.UserPolicy], auto[*users.UserPolicy],
@ -127,7 +134,9 @@ func (wisski *WissKI) allIngredients() []initFunc {
// stacks // stacks
auto[*barrel.Barrel], auto[*barrel.Barrel],
auto[*bookkeeping.Bookkeeping], auto[*bookkeeping.Bookkeeping],
auto[*provisioner.Provisioner], auto[*manager.Manager],
auto[*system.SystemManager],
auto[*composer.Composer],
auto[*drush.Drush], auto[*drush.Drush],
auto[*reserve.Reserve], auto[*reserve.Reserve],