Do a large chunk of the move to go
This commit moves a huge chunk of the code to go. The TODO.md document indicates what is left to be done.
This commit is contained in:
parent
db2ad9b4bd
commit
7b38fdd801
93 changed files with 4689 additions and 645 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
/wdcli
|
||||||
/distillery/overrides.json
|
/distillery/overrides.json
|
||||||
authorized_keys
|
authorized_keys
|
||||||
.vagrant
|
.vagrant
|
||||||
|
|
|
||||||
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
golang 1.18.5
|
||||||
52
TODO.md
Normal file
52
TODO.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# WissKI-Distillery in Go
|
||||||
|
|
||||||
|
This document describes the process of moving the distillery to using golang for the control plane (outside of docker containers).
|
||||||
|
|
||||||
|
## Bootstrapping
|
||||||
|
|
||||||
|
This documents the bootstraping process.
|
||||||
|
Work in progress.
|
||||||
|
|
||||||
|
- `wdcli bootstrap $DIRECTORY`
|
||||||
|
0. Create the deployment directory
|
||||||
|
1. Copy over the executable (unless it already exists)
|
||||||
|
2. Create a default configuration file (unless it already exists)
|
||||||
|
3. Store the directory in a file called .wdcli in the $HOME directory
|
||||||
|
|
||||||
|
- `wdcli system_update`
|
||||||
|
- to be documented
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
- Move `provision_entrypoint.sh` into go
|
||||||
|
- Avoid running `docker compose` executable and shift it to a library
|
||||||
|
- Automatically bootstrap the docker container sql connection (use proper environment variables)
|
||||||
|
- Make error handling consistent
|
||||||
|
- Add a server that serves information
|
||||||
|
- Migrate the individual commands below
|
||||||
|
- restructure resource files
|
||||||
|
- Documentation
|
||||||
|
|
||||||
|
## Migrating Individual Commands
|
||||||
|
- [ ] backup_all.sh
|
||||||
|
- [ ] backup_instance.sh
|
||||||
|
- [x] blind_update.sh
|
||||||
|
- [x] blind_update_all.sh
|
||||||
|
- [x] cron_all.sh
|
||||||
|
- [x] info.sh
|
||||||
|
- [x] ls.sh
|
||||||
|
- [x] make_mysql_account.sh
|
||||||
|
- [ ] monday_full.sh
|
||||||
|
- [ ] monday_short.sh
|
||||||
|
- [x] mysql.sh
|
||||||
|
- [x] provision.sh
|
||||||
|
- [x] purge.sh
|
||||||
|
- [x] rebuild.sh
|
||||||
|
- [x] rebuild_all.sh
|
||||||
|
- [x] reserve.sh
|
||||||
|
- [x] shell.sh
|
||||||
|
- [x] system_install.sh
|
||||||
|
- [x] system_update.sh
|
||||||
|
- [x] update_prefix_config.sh
|
||||||
|
|
||||||
|
## TO BE REMOVED
|
||||||
|
- [ ] call_update_php_hack.sh
|
||||||
54
cmd/blind_update.go
Normal file
54
cmd/blind_update.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlindUpdate is the 'blind-update' command
|
||||||
|
var BlindUpdate wisski_distillery.Command = blindUpdate{}
|
||||||
|
|
||||||
|
type blindUpdate struct {
|
||||||
|
Force bool `short:"f" long:"force" description:"force running blind-update even if AutoBlindUpdate is set to false"`
|
||||||
|
Positionals struct {
|
||||||
|
Slug []string `positional-arg-name:"SLUG" required:"0" description:"slug of instance(s) to run blind-update in"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (blindUpdate) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "blind_update",
|
||||||
|
Description: "Runs the blind update in the provided instances",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBlindUpdateFailed = exit.Error{
|
||||||
|
Message: "Failed to run blind update script for instance %q: exited with code %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bu blindUpdate) Run(context wisski_distillery.Context) error {
|
||||||
|
instances, err := context.Environment.Instances(bu.Positionals.Slug...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range instances {
|
||||||
|
if !(instance.IsBlindUpdateEnabled() || bu.Force) {
|
||||||
|
context.EPrintf("skipping instance %q\n", instance.Slug)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
context.EPrintf("Updating instance %s\n", instance.Slug)
|
||||||
|
|
||||||
|
code := instance.Shell(context.IOStream, "/utils/blind_update.sh")
|
||||||
|
if code != 0 {
|
||||||
|
return errBlindUpdateFailed.WithMessageF(instance.Slug, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
187
cmd/bootstrap.go
Normal file
187
cmd/bootstrap.go
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
cfg "github.com/FAU-CDI/wisski-distillery/internal/config"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/hostname"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/password"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bootstrap is the 'bootstrap' command
|
||||||
|
var Bootstrap wisski_distillery.Command = bootstrap{}
|
||||||
|
|
||||||
|
type bootstrap struct {
|
||||||
|
Directory string `short:"r" long:"root-directory" description:"path to the root deployment directory" default:"/var/www/deploy"`
|
||||||
|
Hostname string `short:"h" long:"hostname" description:"default hostname of the distillery (default: system hostname)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bootstrap) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: false,
|
||||||
|
},
|
||||||
|
Command: "bootstrap",
|
||||||
|
Description: "Bootstraps the installation of a Distillery System",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBootstrapDifferent = exit.Error{
|
||||||
|
Message: "refusing to bootstrap: base directory is already set to %s.",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBootstrapFailedToCreateDirectory = exit.Error{
|
||||||
|
Message: "failed to create directory %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBootstrapFailedToSaveDirectory = exit.Error{
|
||||||
|
Message: "failed to register base directory: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBoostrapFailedToCopyExe = exit.Error{
|
||||||
|
Message: "failed to copy wdcli executable: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBootstrapWriteConfig = exit.Error{
|
||||||
|
Message: "failed to write configuration file: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBootstrapOpenConfig = exit.Error{
|
||||||
|
Message: "failed to open configuration file: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBootstrapCreateFile = exit.Error{
|
||||||
|
Message: "failed to touch configuration file: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs bootstrap) Run(context wisski_distillery.Context) error {
|
||||||
|
root := bs.Directory
|
||||||
|
|
||||||
|
// check that we didn't get a different base directory
|
||||||
|
{
|
||||||
|
got, err := env.ReadBaseDirectory()
|
||||||
|
if err == nil && got != "" && got != root {
|
||||||
|
return errBootstrapDifferent.WithMessageF(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
logging.LogMessage(context.IOStream, "Creating root deployment directory")
|
||||||
|
if err := os.MkdirAll(root, fs.ModeDir); err != nil {
|
||||||
|
return errBootstrapFailedToCreateDirectory.WithMessageF(root)
|
||||||
|
}
|
||||||
|
if err := env.WriteBaseDirectory(root); err != nil {
|
||||||
|
return errBootstrapFailedToSaveDirectory.WithMessageF(root)
|
||||||
|
}
|
||||||
|
context.Println(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Read these from the command line?
|
||||||
|
wdcliPath := filepath.Join(root, "wdcli")
|
||||||
|
envPath := filepath.Join(root, ".env")
|
||||||
|
domain := bs.Hostname
|
||||||
|
if domain == "" {
|
||||||
|
domain = hostname.FQDN()
|
||||||
|
}
|
||||||
|
overridesPath := filepath.Join(root, "overrides.json")
|
||||||
|
authorizedKeysFile := filepath.Join(root, "authorized_keys")
|
||||||
|
|
||||||
|
{
|
||||||
|
logging.LogMessage(context.IOStream, "Copying over wdcli executable")
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return errBoostrapFailedToCopyExe.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fsx.CopyFile(wdcliPath, exe)
|
||||||
|
if err != nil && err != fsx.ErrCopySameFile {
|
||||||
|
return errBoostrapFailedToCopyExe.WithMessageF(err)
|
||||||
|
}
|
||||||
|
context.Println(wdcliPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if !fsx.IsFile(envPath) {
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
password, err := password.Password(128)
|
||||||
|
if err != nil {
|
||||||
|
return errBootstrapWriteConfig.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := distillery.InstallTemplate(envPath, filepath.Join("resources", "templates", "bootstrap", "env"), map[string]string{
|
||||||
|
"DEPLOY_ROOT": root,
|
||||||
|
"DEFAULT_DOMAIN": domain,
|
||||||
|
"SELF_OVERRIDES_FILE": overridesPath,
|
||||||
|
"AUTHORIZED_KEYS_FILE": authorizedKeysFile,
|
||||||
|
|
||||||
|
"GRAPHDB_ADMIN_USER": "admin",
|
||||||
|
"GRAPHDB_ADMIN_PASSWORD": password[:64],
|
||||||
|
|
||||||
|
"MYSQL_ADMIN_USER": "admin",
|
||||||
|
"MYSQL_ADMIN_PASSWORD": password[64:],
|
||||||
|
}); err != nil {
|
||||||
|
return errBootstrapWriteConfig.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Installing configuration file"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
|
||||||
|
context.Println(overridesPath)
|
||||||
|
if err := distillery.InstallTemplate(overridesPath, filepath.Join("resources", "templates", "bootstrap", "overrides.json"), map[string]string{}); err != nil {
|
||||||
|
return errBootstrapCreateFile.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Println(authorizedKeysFile)
|
||||||
|
if err := distillery.InstallTemplate(authorizedKeysFile, filepath.Join("resources", "templates", "bootstrap", "global_authorized_keys"), map[string]string{}); err != nil {
|
||||||
|
return errBootstrapCreateFile.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Creating additional config files"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-read the configuration and print it!
|
||||||
|
logging.LogMessage(context.IOStream, "Configuration is now complete")
|
||||||
|
f, err := os.Open(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return errBootstrapOpenConfig.WithMessageF(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var config cfg.Config
|
||||||
|
if err := config.Unmarshal(f); err != nil {
|
||||||
|
return errBootstrapOpenConfig.WithMessageF(err)
|
||||||
|
}
|
||||||
|
context.Println(config)
|
||||||
|
|
||||||
|
// Tell the user how to proceed
|
||||||
|
logging.LogMessage(context.IOStream, "Bootstrap is complete")
|
||||||
|
context.Printf("Adjust the configuration file at %s\n", envPath)
|
||||||
|
context.Printf("Then grab a GraphDB zipped source file and run:\n")
|
||||||
|
context.Printf("%s system_update /path/to/graphdb.zip\n", wdcliPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
27
cmd/config.go
Normal file
27
cmd/config.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the configuration command
|
||||||
|
var Config wisski_distillery.Command = config{}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s config) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "config",
|
||||||
|
Description: "Prints information about configuration",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s config) Run(context wisski_distillery.Context) error {
|
||||||
|
context.Printf("%#v", context.Environment.Config)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
cmd/cron.go
Normal file
55
cmd/cron.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cron is the 'cron' command
|
||||||
|
var Cron wisski_distillery.Command = cron{}
|
||||||
|
|
||||||
|
type cron struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug []string `positional-arg-name:"SLUG" required:"0" description:"slug of instance(s) to run cron in"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cron) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "cron",
|
||||||
|
Description: "Runs the cron script for several instances",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errCronFailed = exit.Error{
|
||||||
|
Message: "Failed to run cron script for instance %q: exited with code %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr cron) Run(context wisski_distillery.Context) error {
|
||||||
|
instances, err := context.Environment.Instances(cr.Positionals.Slug...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over the instances and store the last value of error
|
||||||
|
for _, instance := range instances {
|
||||||
|
logging.LogOperation(func() error {
|
||||||
|
code := instance.Shell(context.IOStream, "/utils/cron.sh")
|
||||||
|
if code != 0 {
|
||||||
|
// keep going, because we want to run as many crons as possible
|
||||||
|
err = errBlindUpdateFailed.WithMessageF(instance.Slug, code)
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "running cron for instance %s", instance.Slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
45
cmd/info.go
Normal file
45
cmd/info.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Info is then 'info' command
|
||||||
|
var Info wisski_distillery.Command = info{}
|
||||||
|
|
||||||
|
type info struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug string `positional-arg-name:"SLUG" required:"1-1" description:"slug of instance to show info about"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "info",
|
||||||
|
Description: "Provide information about a single repository",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i info) Run(context wisski_distillery.Context) error {
|
||||||
|
instance, err := context.Environment.Instance(i.Positionals.Slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Printf("URL: %s\n", instance.URL())
|
||||||
|
context.Printf("Base directory: %s\n", instance.FilesystemBase)
|
||||||
|
|
||||||
|
context.Printf("SQL Database: %s\n", instance.SqlDatabase)
|
||||||
|
context.Printf("SQL Username: %s\n", instance.SqlUser)
|
||||||
|
context.Printf("SQL Password: %s\n", instance.SqlPassword)
|
||||||
|
|
||||||
|
context.Printf("GraphDB Repository: %s\n", instance.GraphDBRepository)
|
||||||
|
context.Printf("GraphDB Username: %s\n", instance.GraphDBUser)
|
||||||
|
context.Printf("GraphDB Password: %s\n", instance.GraphDBPassword)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
47
cmd/license.go
Normal file
47
cmd/license.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/legal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// License is the 'wdcli license' command.
|
||||||
|
//
|
||||||
|
// The license command prints to standard output legal notices about the wdcli program.
|
||||||
|
var License wisski_distillery.Command = license{}
|
||||||
|
|
||||||
|
type license struct{}
|
||||||
|
|
||||||
|
func (license) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: false,
|
||||||
|
},
|
||||||
|
Command: "license",
|
||||||
|
Description: "Print licensing information about wdcli and exit. ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (license) AfterParse() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (license) Run(context wisski_distillery.Context) error {
|
||||||
|
context.Printf(stringLicenseInfo, wisski_distillery.License, legal.Notices)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringLicenseInfo = `
|
||||||
|
wdcli -- WissKI Distillery Command Line Utility
|
||||||
|
https://github.com/FAU-CDI/wisski-distillery
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
wdcli is licensed under the terms of the AGPL Version 3.0 License:
|
||||||
|
|
||||||
|
%s
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Furthermore, this executable may include code from the following projects:
|
||||||
|
%s
|
||||||
|
`
|
||||||
38
cmd/ls.go
Normal file
38
cmd/ls.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ls is the 'ls' command
|
||||||
|
var Ls wisski_distillery.Command = ls{}
|
||||||
|
|
||||||
|
type ls struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug []string `positional-arg-name:"SLUG" required:"0" description:"slug(s) of instance(s) to list"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "ls",
|
||||||
|
Description: "Lists WissKI instances",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l ls) Run(context wisski_distillery.Context) error {
|
||||||
|
instances, err := context.Environment.Instances(l.Positionals.Slug...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, instance := range instances {
|
||||||
|
context.Println(instance.Slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
67
cmd/make_mysql_account.go
Normal file
67
cmd/make_mysql_account.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/sqle"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shell is the 'shell' command
|
||||||
|
var MakeMysqlAccount wisski_distillery.Command = makeMysqlAccount{}
|
||||||
|
|
||||||
|
type makeMysqlAccount struct{}
|
||||||
|
|
||||||
|
func (makeMysqlAccount) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
ParserConfig: parser.Config{
|
||||||
|
IncludeUnknown: true,
|
||||||
|
},
|
||||||
|
Command: "make_mysql_account",
|
||||||
|
Description: "Open a shell in the provided instance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errUnableToReadUsername = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
Message: "unable to read username: %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
var errUnableToReadPassword = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
Message: "unable to read password: %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mma makeMysqlAccount) Run(context wisski_distillery.Context) error {
|
||||||
|
context.Printf("Username>")
|
||||||
|
username, err := context.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return errUnableToReadUsername.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Printf("Password>")
|
||||||
|
password, err := context.ReadPassword()
|
||||||
|
if err != nil {
|
||||||
|
return errUnableToReadPassword.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := sqle.Format("CREATE USER ?@'%' IDENTIFIED BY ?; GRANT ALL PRIVILEGES ON *.* TO ?@`%` WITH GRANT OPTION; FLUSH PRIVILEGES;", username, password, username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
code := context.Environment.SQLShell(context.IOStream, "-e", query)
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
return exit.Error{
|
||||||
|
ExitCode: exit.ExitCode(uint8(code)),
|
||||||
|
Message: fmt.Sprintf("Exit code %d", code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
43
cmd/mysql.go
Normal file
43
cmd/mysql.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mysql is the 'mysql' command
|
||||||
|
var Mysql wisski_distillery.Command = mysql{}
|
||||||
|
|
||||||
|
type mysql struct {
|
||||||
|
Positionals struct {
|
||||||
|
Args []string `positional-arg-name:"ARGS" description:"arguments to pass to the mysql command"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mysql) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
ParserConfig: parser.Config{
|
||||||
|
IncludeUnknown: true,
|
||||||
|
},
|
||||||
|
Command: "mysql",
|
||||||
|
Description: "Opens a mysql shell",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms mysql) Run(context wisski_distillery.Context) error {
|
||||||
|
code := context.Environment.SQLShell(context.IOStream, ms.Positionals.Args...)
|
||||||
|
if code != 0 {
|
||||||
|
return exit.Error{
|
||||||
|
ExitCode: exit.ExitCode(uint8(code)),
|
||||||
|
Message: fmt.Sprintf("Exit code %d", code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
77
cmd/prefix.go
Normal file
77
cmd/prefix.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cron is the 'cron' command
|
||||||
|
var UpdatePrefixConfig wisski_distillery.Command = updateprefixconfig{}
|
||||||
|
|
||||||
|
type updateprefixconfig struct{}
|
||||||
|
|
||||||
|
func (updateprefixconfig) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "update_prefix_config",
|
||||||
|
Description: "Updates the prefix configuration",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errPrefixUpdateFailed = exit.Error{
|
||||||
|
Message: "Failed to update the prefix configuration: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (upc updateprefixconfig) Run(context wisski_distillery.Context) error {
|
||||||
|
dis := context.Environment
|
||||||
|
|
||||||
|
instances, err := dis.AllInstances()
|
||||||
|
if err != nil {
|
||||||
|
return errPrefixUpdateFailed.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := dis.ResolverPrefixConfig()
|
||||||
|
|
||||||
|
// print the configuration
|
||||||
|
config, err := os.OpenFile(target, os.O_WRONLY, fs.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return errPrefixUpdateFailed.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over the instances and store the last value of error
|
||||||
|
for _, instance := range instances {
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
// read the prefix config
|
||||||
|
data, err := instance.PrefixConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
context.IOStream.Printf("%s", data)
|
||||||
|
|
||||||
|
// and write it out!
|
||||||
|
if _, err := config.WriteString(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "reading prefix config %s", instance.Slug); err != nil {
|
||||||
|
return errPrefixUpdateFailed.WithMessageF(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// and restart the resolver to apply the config!
|
||||||
|
logging.LogMessage(context.IOStream, "restarting resolver stack")
|
||||||
|
if err := dis.ResolverStack().Restart(context.IOStream); err != nil {
|
||||||
|
return errPrefixUpdateFailed.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
121
cmd/provision.go
Normal file
121
cmd/provision.go
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provision is the 'provision' command
|
||||||
|
var Provision wisski_distillery.Command = provision{}
|
||||||
|
|
||||||
|
type provision struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug string `positional-arg-name:"slug" required:"1-1" description:"name of WissKI Instance to create"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provision) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "provision",
|
||||||
|
Description: "Creates a new WissKI Instance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: AfterParse to check instance!
|
||||||
|
|
||||||
|
var errProvisionAlreadyExists = exit.Error{
|
||||||
|
Message: "Instance %q already exists",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errProvisionGeneric = exit.Error{
|
||||||
|
Message: "Unable to provision instance %s: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p provision) Run(context wisski_distillery.Context) error {
|
||||||
|
dis := context.Environment
|
||||||
|
slug := p.Positionals.Slug
|
||||||
|
|
||||||
|
// check that it doesn't already exist
|
||||||
|
logging.LogMessage(context.IOStream, "Provisioning new WissKI instance %s", slug)
|
||||||
|
if exists, err := dis.HasInstance(slug); err != nil || exists {
|
||||||
|
return errProvisionAlreadyExists.WithMessageF(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it in-memory
|
||||||
|
instance, err := dis.NewInstance(slug)
|
||||||
|
if err != nil {
|
||||||
|
return errProvisionGeneric.WithMessageF(slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the base directory does not exist
|
||||||
|
logging.LogMessage(context.IOStream, "Checking that base directory %s does not exist", instance.FilesystemBase)
|
||||||
|
if fsx.IsDirectory(instance.FilesystemBase) {
|
||||||
|
return errProvisionAlreadyExists.WithMessageF(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in bookkeeping
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := instance.Update(); err != nil {
|
||||||
|
return errProvisionGeneric.WithMessageF(slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Updating bookkeeping database"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the sql
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := dis.SQLProvision(instance.SqlDatabase, instance.SqlUser, instance.SqlPassword); err != nil {
|
||||||
|
return errProvisionGeneric.WithMessageF(slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Provisioning SQL Database"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the triplestore
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := dis.TriplestoreProvision(instance.GraphDBRepository, instance.Domain(), instance.GraphDBUser, instance.GraphDBPassword); err != nil {
|
||||||
|
return errProvisionGeneric.WithMessageF(slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Provisioning Triplestore"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the provision script
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := instance.Provision(context.IOStream); err != nil {
|
||||||
|
return errProvisionGeneric.WithMessageF(slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Running setup scripts"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// start the container!
|
||||||
|
logging.LogMessage(context.IOStream, "Starting Container")
|
||||||
|
if err := instance.Stack().Up(context.IOStream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// and we're done!
|
||||||
|
logging.LogMessage(context.IOStream, "Instance has been provisioned")
|
||||||
|
context.Printf("URL: %s\n", instance.URL().String())
|
||||||
|
context.Printf("Username: %s\n", instance.DrupalUsername)
|
||||||
|
context.Printf("Password: %s\n", instance.DrupalPassword)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
117
cmd/purge.go
Normal file
117
cmd/purge.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provision is the 'provision' command
|
||||||
|
var Purge wisski_distillery.Command = purge{}
|
||||||
|
|
||||||
|
type purge struct {
|
||||||
|
Yes bool `short:"y" long:"yes" description:"Skip asking for confirmation"`
|
||||||
|
Positionals struct {
|
||||||
|
Slug string `positional-arg-name:"slug" required:"1-1" description:"name of WissKI Instance to purge"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (purge) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "purge",
|
||||||
|
Description: "Purges a WissKI Instance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errPurgeNoDetails = exit.Error{
|
||||||
|
Message: "Unable to find instance details for purge: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errPurgeNoConfirmation = exit.Error{
|
||||||
|
Message: "Aborting after request was not confirmed. Either type `yes` or pass `--yes` on the command line",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p purge) Run(context wisski_distillery.Context) error {
|
||||||
|
dis := context.Environment
|
||||||
|
slug := p.Positionals.Slug
|
||||||
|
|
||||||
|
// check the confirmation from the user
|
||||||
|
if !p.Yes {
|
||||||
|
context.Printf("About to remove repository %s. This cannot be undone.\n", slug)
|
||||||
|
context.Printf("Type 'yes' to continue: ")
|
||||||
|
line, err := context.ReadLine()
|
||||||
|
if err != nil || line != "yes" {
|
||||||
|
return errPurgeNoConfirmation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the instance (first via bookkeeping, then via defaults)
|
||||||
|
logging.LogMessage(context.IOStream, "Checking bookkeeping table")
|
||||||
|
instance, err := dis.Instance(slug)
|
||||||
|
if err == env.ErrInstanceNotFound {
|
||||||
|
context.Println("Not found in bookkeeping table, assuming defaults")
|
||||||
|
instance, err = dis.NewInstance(slug)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errPurgeNoDetails.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove docker stack
|
||||||
|
logging.LogMessage(context.IOStream, "Stopping and removing docker container")
|
||||||
|
if err := instance.Stack().Down(context.IOStream); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the filesystem
|
||||||
|
logging.LogMessage(context.IOStream, "Removing from filesystem %s", instance.FilesystemBase)
|
||||||
|
if err := os.RemoveAll(instance.FilesystemBase); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the triplestore
|
||||||
|
logging.LogOperation(func() error {
|
||||||
|
logging.LogMessage(context.IOStream, "Removing user %s", instance.GraphDBUser)
|
||||||
|
if err := dis.TriplestorePurgeUser(instance.GraphDBUser); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.LogMessage(context.IOStream, "Removing repository %s", instance.GraphDBRepository)
|
||||||
|
if err := dis.TriplestorePurgeRepo(instance.GraphDBRepository); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Removing from Triplestore")
|
||||||
|
|
||||||
|
// remove the sql
|
||||||
|
logging.LogOperation(func() error {
|
||||||
|
logging.LogMessage(context.IOStream, "Removing user %s", instance.SqlUser)
|
||||||
|
if err := dis.SQLPurgeUser(instance.SqlUser); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.LogMessage(context.IOStream, "Removing database %s", instance.SqlDatabase)
|
||||||
|
if err := dis.SQLPurgeDatabase(instance.SqlDatabase); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Removing from SQL")
|
||||||
|
|
||||||
|
// remove from bookkeeping
|
||||||
|
logging.LogMessage(context.IOStream, "Removing instance from bookkeeping")
|
||||||
|
if err := instance.Delete(); err != nil {
|
||||||
|
context.EPrintln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.LogMessage(context.IOStream, "Instance %s has been purged", slug)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
65
cmd/rebuild.go
Normal file
65
cmd/rebuild.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cron is the 'cron' command
|
||||||
|
var Rebuild wisski_distillery.Command = rebuild{}
|
||||||
|
|
||||||
|
type rebuild struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug []string `positional-arg-name:"SLUG" required:"0" description:"slug of instance(s) to run rebuild"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rebuild) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "rebuild",
|
||||||
|
Description: "Runs the rebuild script for several instances",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errRebuildFailed = exit.Error{
|
||||||
|
Message: "Failed to run rebuild script for instance %q: exited with code %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb rebuild) Run(context wisski_distillery.Context) error {
|
||||||
|
instances, err := context.Environment.Instances(rb.Positionals.Slug...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over the instances and store the last value of error
|
||||||
|
var globalErr error
|
||||||
|
for _, instance := range instances {
|
||||||
|
logging.LogOperation(func() error {
|
||||||
|
s := instance.Stack()
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
return s.Install(context.IOStream, stack.InstallationContext{})
|
||||||
|
}, context.IOStream, "Installing docker stack"); err != nil {
|
||||||
|
globalErr = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
return s.Update(context.IOStream, true)
|
||||||
|
}, context.IOStream, "Updating docker stack"); err != nil {
|
||||||
|
globalErr = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Rebuilding instance %s", instance.Slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalErr
|
||||||
|
}
|
||||||
86
cmd/reserve.go
Normal file
86
cmd/reserve.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reserve is the 'reserve' command
|
||||||
|
var Reserve wisski_distillery.Command = reserve{}
|
||||||
|
|
||||||
|
type reserve struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug string `positional-arg-name:"slug" required:"1-1" description:"name of WissKI Instance to reserve"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (reserve) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
Command: "reserve",
|
||||||
|
Description: "Reserves a new WissKI Instance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: AfterParse to check instance!
|
||||||
|
|
||||||
|
var errReserveAlreadyExists = exit.Error{
|
||||||
|
Message: "Instance %q already exists",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errReserveGeneric = exit.Error{
|
||||||
|
Message: "Unable to provision instance %s: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reserve) Run(context wisski_distillery.Context) error {
|
||||||
|
dis := context.Environment
|
||||||
|
slug := r.Positionals.Slug
|
||||||
|
|
||||||
|
// check that it doesn't already exist
|
||||||
|
logging.LogMessage(context.IOStream, "Reserving new WissKI instance %s", slug)
|
||||||
|
if exists, err := dis.HasInstance(slug); err != nil || exists {
|
||||||
|
return errProvisionAlreadyExists.WithMessageF(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make it in-memory
|
||||||
|
instance, err := dis.NewInstance(slug)
|
||||||
|
if err != nil {
|
||||||
|
return errProvisionGeneric.WithMessageF(slug, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the base directory does not exist
|
||||||
|
logging.LogMessage(context.IOStream, "Checking that base directory %s does not exist", instance.FilesystemBase)
|
||||||
|
if fsx.IsDirectory(instance.FilesystemBase) {
|
||||||
|
return errProvisionAlreadyExists.WithMessageF(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup docker stack
|
||||||
|
s := instance.ReserveStack()
|
||||||
|
{
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
return s.Install(context.IOStream, stack.InstallationContext{})
|
||||||
|
}, context.IOStream, "Installing docker stack"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
return s.Update(context.IOStream, true)
|
||||||
|
}, context.IOStream, "Updating docker stack"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// and we're done!
|
||||||
|
logging.LogMessage(context.IOStream, "Instance has been reserved")
|
||||||
|
context.Printf("URL: %s\n", instance.URL().String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
cmd/shell.go
Normal file
49
cmd/shell.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shell is the 'shell' command
|
||||||
|
var Shell wisski_distillery.Command = shell{}
|
||||||
|
|
||||||
|
type shell struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug string `positional-arg-name:"SLUG" required:"1-1" description:"slug of instance to show run shell in"`
|
||||||
|
Args []string `positional-arg-name:"ARGS" description:"arguments to pass to the shell"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shell) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
ParserConfig: parser.Config{
|
||||||
|
IncludeUnknown: true,
|
||||||
|
},
|
||||||
|
Command: "shell",
|
||||||
|
Description: "Open a shell in the provided instance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sh shell) Run(context wisski_distillery.Context) error {
|
||||||
|
instance, err := context.Environment.Instance(sh.Positionals.Slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := instance.Shell(context.IOStream, sh.Positionals.Args...)
|
||||||
|
if code != 0 {
|
||||||
|
return exit.Error{
|
||||||
|
ExitCode: exit.ExitCode(uint8(code)),
|
||||||
|
Message: fmt.Sprintf("Exit code %d", code),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
182
cmd/system_update.go
Normal file
182
cmd/system_update.go
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/execx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SystemUpdate is the 'system_update' command
|
||||||
|
var SystemUpdate wisski_distillery.Command = systemupdate{}
|
||||||
|
|
||||||
|
type systemupdate struct {
|
||||||
|
SkipCoreUpdates bool `short:"s" long:"skip-core-updates" description:"Skip applying operating system and other core system updates"`
|
||||||
|
Positionals struct {
|
||||||
|
GraphdbZip string `positional-arg-name:"PATH_TO_GRAPHDB_ZIP" required:"1-1" description:"path to the graphdb.zip file"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (systemupdate) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: env.Requirements{
|
||||||
|
NeedsConfig: true,
|
||||||
|
},
|
||||||
|
ParserConfig: parser.Config{
|
||||||
|
IncludeUnknown: true,
|
||||||
|
},
|
||||||
|
Command: "system_update",
|
||||||
|
Description: "Installs and Update Components of the WissKI Distillery System",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNoGraphDBZip = exit.Error{
|
||||||
|
Message: "%s does not exist",
|
||||||
|
ExitCode: exit.ExitCommandArguments,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s systemupdate) AfterParse() error {
|
||||||
|
_, err := os.Stat(s.Positionals.GraphdbZip)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return errNoGraphDBZip.WithMessageF(s.Positionals.GraphdbZip)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errFailedToCreateDirectory = exit.Error{
|
||||||
|
Message: "failed to create directory %s: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errFailedRuntime = exit.Error{
|
||||||
|
Message: "failed to update runtime: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si systemupdate) Run(context wisski_distillery.Context) error {
|
||||||
|
dis := context.Environment
|
||||||
|
|
||||||
|
// create all the other directories
|
||||||
|
logging.LogMessage(context.IOStream, "Ensuring distillery installation directories exist")
|
||||||
|
for _, d := range []string{
|
||||||
|
dis.Config.DeployRoot,
|
||||||
|
dis.InstancesDir(),
|
||||||
|
dis.InprogressBackupPath(),
|
||||||
|
dis.FinalBackupPath(),
|
||||||
|
} {
|
||||||
|
context.Println(d)
|
||||||
|
if err := os.MkdirAll(d, os.ModeDir); err != nil {
|
||||||
|
return errFailedToCreateDirectory.WithMessageF(d, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !si.SkipCoreUpdates {
|
||||||
|
// install system updates
|
||||||
|
logging.LogMessage(context.IOStream, "Updating Operating System Packages")
|
||||||
|
if err := si.mustExec(context, "", "apt-get", "update"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := si.mustExec(context, "", "apt-get", "upgrade", "-y"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// install docker
|
||||||
|
logging.LogMessage(context.IOStream, "Installing / Updating Docker")
|
||||||
|
if err := si.mustExec(context, "", "apt-get", "install", "curl"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := si.mustExec(context, "", "/bin/sh", "-c", "curl -fsSL https://get.docker.com -o - | /bin/sh"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the docker network
|
||||||
|
// TODO: Use docker API for this
|
||||||
|
logging.LogMessage(context.IOStream, "Updating Docker Configuration")
|
||||||
|
si.mustExec(context, "", "docker", "network", "create", "distillery")
|
||||||
|
|
||||||
|
// install and update the various stacks!
|
||||||
|
ctx := stack.InstallationContext{
|
||||||
|
"graphdb.zip": si.Positionals.GraphdbZip,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
for _, stack := range dis.Stacks() {
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
return stack.Install(context.IOStream, ctx)
|
||||||
|
}, context.IOStream, "Installing docker stack %q", stack.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
return stack.Update(context.IOStream, true)
|
||||||
|
}, context.IOStream, "Updating docker stack %q", stack.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Updating Components"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := distillery.InstallResource(dis.RuntimeDir(), filepath.Join("resources", "runtime"), func(dst, src string) {
|
||||||
|
context.Printf("[copy] %s\n", dst)
|
||||||
|
}); err != nil {
|
||||||
|
return errFailedRuntime.WithMessageF(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Unpacking Runtime Components"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := dis.SQLBootstrap(context.IOStream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Bootstraping SQL database"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logging.LogOperation(func() error {
|
||||||
|
if err := dis.TriplestoreBootstrap(context.IOStream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, context.IOStream, "Bootstraping Triplestore"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.LogMessage(context.IOStream, "System has been updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errMustExecFailed = exit.Error{
|
||||||
|
Message: "process exited with code %d",
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustExec indicates that the given executable process must complete successfully.
|
||||||
|
// If it does not, returns errMustExecFailed
|
||||||
|
func (si systemupdate) mustExec(context wisski_distillery.Context, workdir string, exe string, argv ...string) error {
|
||||||
|
if workdir == "" {
|
||||||
|
workdir = context.Environment.Config.DeployRoot
|
||||||
|
}
|
||||||
|
code := execx.Exec(context.IOStream, workdir, exe, argv...)
|
||||||
|
if code != 0 {
|
||||||
|
err := errMustExecFailed.WithMessageF(code)
|
||||||
|
err.ExitCode = exit.ExitCode(code)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
110
cmd/wdcli/main.go
Normal file
110
cmd/wdcli/main.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Command wdcli implement the entry point for the wisski-distillery
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/cmd"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wdcli = wisski_distillery.NewProgram()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// self commands
|
||||||
|
wdcli.Register(cmd.Config)
|
||||||
|
wdcli.Register(cmd.License)
|
||||||
|
|
||||||
|
// setup commands
|
||||||
|
wdcli.Register(cmd.Bootstrap)
|
||||||
|
wdcli.Register(cmd.SystemUpdate)
|
||||||
|
|
||||||
|
// sql commands
|
||||||
|
wdcli.Register(cmd.Mysql)
|
||||||
|
wdcli.Register(cmd.MakeMysqlAccount)
|
||||||
|
|
||||||
|
// instance setup and teardown
|
||||||
|
wdcli.Register(cmd.Provision)
|
||||||
|
wdcli.Register(cmd.Purge)
|
||||||
|
wdcli.Register(cmd.Reserve)
|
||||||
|
wdcli.Register(cmd.Rebuild)
|
||||||
|
|
||||||
|
// instance management
|
||||||
|
wdcli.Register(cmd.Ls)
|
||||||
|
wdcli.Register(cmd.Info)
|
||||||
|
|
||||||
|
// instance tasks
|
||||||
|
wdcli.Register(cmd.Shell)
|
||||||
|
wdcli.Register(cmd.BlindUpdate)
|
||||||
|
wdcli.Register(cmd.Cron)
|
||||||
|
wdcli.Register(cmd.UpdatePrefixConfig) // TODO: Move into post-instance configuration
|
||||||
|
|
||||||
|
// backup & cron
|
||||||
|
// wdcli.Register(cmd.BackupInstance)
|
||||||
|
// wdcli.Register(cmd.BackupAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
// an error when no arguments are provided.
|
||||||
|
var errNoArgumentsProvided = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneralArguments,
|
||||||
|
Message: "Need at least one argument. Use `wdcli license` to view licensing information. ",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// recover from calls to panic(), and exit the program appropriatly.
|
||||||
|
// This has to be in the main() function because any of the library functions might be broken.
|
||||||
|
// For this reason, as few ggman functions as possible are used here; just stuff from the top-level ggman package.
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, fatalPanicMessage, err)
|
||||||
|
debug.PrintStack()
|
||||||
|
exit.ExitPanic.Return()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
streams := stream.FromEnv()
|
||||||
|
|
||||||
|
// when there are no arguments then parsing argument *will* fail
|
||||||
|
//
|
||||||
|
// we don't need to even bother with the rest of the program
|
||||||
|
// just immediatly return a custom error message.
|
||||||
|
if len(os.Args) == 1 {
|
||||||
|
streams.Die(errNoArgumentsProvided)
|
||||||
|
errNoArgumentsProvided.Return()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// creat a new set of parameters
|
||||||
|
// and then use them to execute the main command
|
||||||
|
err := func() error {
|
||||||
|
params, err := env.ParamsFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
return streams.Die(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wdcli.Main(streams, params, os.Args[1:])
|
||||||
|
}()
|
||||||
|
|
||||||
|
// return the error to the user
|
||||||
|
|
||||||
|
exit.AsError(err).Return()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fatalPanicMessage = `Fatal Error: Panic
|
||||||
|
|
||||||
|
The wdcli program panicked and had to abort execution. This is usually
|
||||||
|
indicative of a bug. If this occurs repeatedly you might want to consider
|
||||||
|
filing an issue in the issue tracker at:
|
||||||
|
|
||||||
|
https://github.com/FAU-CDI/wisski-distillery/issues
|
||||||
|
|
||||||
|
Below is debug information that might help the developers track down what
|
||||||
|
happened.
|
||||||
|
|
||||||
|
panic: %v
|
||||||
|
`
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
|
|
||||||
# if the site doesn't exist, I can't open a shell.
|
|
||||||
if ! sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' does not exist in bookeeping table. "
|
|
||||||
echo "I can't rebuild it. "
|
|
||||||
exit 1
|
|
||||||
fi;
|
|
||||||
|
|
||||||
# Read everything from the database
|
|
||||||
read -r INSTANCE_BASE_DIR MYSQL_DATABASE MYSQL_USER GRAPHDB_REPO GRAPHDB_USER <<< "$(sql_bookkeep_load "${SLUG}" "filesystem_base,sql_database,sql_user,graphdb_repository,graphdb_user" | tail -n +2)"
|
|
||||||
|
|
||||||
# cd into the right directory
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
||||||
# and open a www-data shell
|
|
||||||
docker-compose exec barrel /user_shell.sh /utils/blind_update.sh
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
DISABLE_LOG=0
|
|
||||||
|
|
||||||
# update all the instances
|
|
||||||
for slug in $(sql_bookkeep_list_updateable); do
|
|
||||||
log_info "=> /bin/bash $DIR/blind_update.sh '$slug'"
|
|
||||||
/bin/bash "$DIR/blind_update.sh" "$slug";
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
unset DISABLE_LOG
|
|
||||||
|
|
||||||
# update all the instances
|
|
||||||
for slug in $(sql_bookkeep_list); do
|
|
||||||
read -r INSTANCE_BASE_DIR <<< "$(sql_bookkeep_load "${slug}" "filesystem_base" | tail -n +2)"
|
|
||||||
log_info "=> Runnning cron for '$slug'"
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
docker-compose exec barrel /bin/bash /utils/cron.sh
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
DISABLE_LOG=0
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
|
|
||||||
# if the site doesn't exist, I can't open a shell.
|
|
||||||
if ! sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' does not exist in bookeeping table. "
|
|
||||||
echo "I can't show info about it. "
|
|
||||||
exit 1
|
|
||||||
fi;
|
|
||||||
|
|
||||||
# Read everything from the database
|
|
||||||
read -r INSTANCE_BASE_DIR MYSQL_DATABASE MYSQL_USER GRAPHDB_REPO GRAPHDB_USER GRAPHDB_PASSWORD <<< "$(sql_bookkeep_load "${SLUG}" "filesystem_base,sql_database,sql_user,graphdb_repository,graphdb_user,graphdb_password" | tail -n +2)"
|
|
||||||
|
|
||||||
GRAPHDB_HEADER="$(printf "%s:%s" "$GRAPHDB_USER" "$GRAPHDB_PASSWORD" | base64 -w 0)"
|
|
||||||
|
|
||||||
# read sql configuration
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
read -r SQL_DATABASE SQL_USER SQL_PASS SQL_OTHER <<< "$(docker-compose exec barrel drush sql:conf --format=tsv --show-passwords)"
|
|
||||||
|
|
||||||
echo "=================================================================================="
|
|
||||||
echo "URL: http://$INSTANCE_DOMAIN"
|
|
||||||
echo "Base directory: ${INSTANCE_BASE_DIR}"
|
|
||||||
log_info " => Your GraphDB details (for WissKI Salz) are: "
|
|
||||||
echo "Read URL: http://triplestore:7200/repositories/$GRAPHDB_REPO"
|
|
||||||
echo "Write URL: http://triplestore:7200/repositories/$GRAPHDB_REPO/statements"
|
|
||||||
echo "Username: $GRAPHDB_USER"
|
|
||||||
echo "Password: $GRAPHDB_PASSWORD"
|
|
||||||
echo "Authorization Header: $GRAPHDB_HEADER"
|
|
||||||
echo "Writable: yes"
|
|
||||||
echo "Default Graph URI: http://$INSTANCE_DOMAIN/#"
|
|
||||||
echo "Ontology Paths: (empty)"
|
|
||||||
echo "SameAs property: http://www.w3.org/2002/07/owl#sameAs"
|
|
||||||
log_info " => Your SQL detsils are: "
|
|
||||||
echo "SQL Database: $SQL_DATABASE"
|
|
||||||
echo "SQL Username: $SQL_USER"
|
|
||||||
echo "SQL Password: $SQL_PASS"
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
DISABLE_LOG=0
|
|
||||||
|
|
||||||
# list all the existing instances
|
|
||||||
sql_bookkeep_list
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh
|
|
||||||
DISABLE_LOG=0
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
|
|
||||||
# wait for sql to come up
|
|
||||||
wait_for_sql > /dev/null
|
|
||||||
|
|
||||||
echo "Creating new MySQL user with root privileges. "
|
|
||||||
read -p 'Enter Username:' MYSQL_USER
|
|
||||||
read -sp 'Enter password:' MYSQL_PASSWORD
|
|
||||||
|
|
||||||
if ! is_valid_slug "$MYSQL_USER"; then
|
|
||||||
echo "Not a valid username: ${MYSQL_USER}"
|
|
||||||
echo "User must be alphanumeric for sql injection reasons. "
|
|
||||||
echo "You can always create a user manually. "
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! is_valid_slug "$MYSQL_PASSWORD"; then
|
|
||||||
echo "Not a valid password: ${MYSQL_PASSWORD}"
|
|
||||||
echo "Password must be alphanumeric for sql injection reasons. "
|
|
||||||
echo "You can always create a user manually. "
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
dockerized_mysql -e "CREATE USER \`${MYSQL_USER}\`@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; GRANT ALL PRIVILEGES ON *.* TO \`${MYSQL_USER}\`@\`%\` WITH GRANT OPTION; FLUSH PRIVILEGES;"
|
|
||||||
|
|
||||||
log_info "Created user ${MYSQL_USER}"
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
|
|
||||||
# wait for sql to come up
|
|
||||||
wait_for_sql > /dev/null
|
|
||||||
dockerized_mysql_interactive "$@"
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
# wait for sql to be awake
|
|
||||||
wait_for_sql
|
|
||||||
|
|
||||||
# check if the site exists
|
|
||||||
if sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' already exists in bookeeping table. "
|
|
||||||
echo "Refusing to work"
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Randomly generate the database name and user we will configure.
|
|
||||||
# Use the 'randompw' alias for this.
|
|
||||||
log_info " => Generating new MySQL password"
|
|
||||||
MYSQL_PASSWORD="$(randompw)"
|
|
||||||
|
|
||||||
# Initialize the SQL database with those credentials.
|
|
||||||
log_info " => Intializing new SQL database '${MYSQL_DATABASE}' and user '$MYSQL_USER'. "
|
|
||||||
dockerized_mysql -e "CREATE DATABASE \`${MYSQL_DATABASE}\`;"
|
|
||||||
dockerized_mysql -e "CREATE USER \`${MYSQL_USER}\`@'%' IDENTIFIED BY '${MYSQL_PASSWORD}';"
|
|
||||||
dockerized_mysql -e "GRANT ALL PRIVILEGES ON \`${MYSQL_DATABASE}\`.* TO \`${MYSQL_USER}\`@\`%\`;"
|
|
||||||
dockerized_mysql -e "FLUSH PRIVILEGES;"
|
|
||||||
|
|
||||||
# Create a new repository for GraphDB.
|
|
||||||
# Use the template for this.
|
|
||||||
log_info " => Generating new GraphDB repository '$GRAPHDB_REPO'"
|
|
||||||
load_template "repository/graphdb-repo.ttl" "GRAPHDB_REPO" "${GRAPHDB_REPO}" "INSTANCE_DOMAIN" "${INSTANCE_DOMAIN}" | \
|
|
||||||
curl -X POST $GRAPHDB_AUTH_FLAGS \
|
|
||||||
http://127.0.0.1:7200/rest/repositories \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
-F "config=@-"
|
|
||||||
|
|
||||||
# Generate a random password for the GraphDB user
|
|
||||||
log_info " => Generating a new GraphDB password"
|
|
||||||
GRAPHDB_PASSWORD="$(randompw)"
|
|
||||||
|
|
||||||
# Create the user and grant them access to the creatd database.
|
|
||||||
log_info " => Creating GraphDB user '$GRAPHDB_USER'"
|
|
||||||
load_template "repository/graphdb-user.json" "GRAPHDB_USER" "${GRAPHDB_USER}" "GRAPHDB_REPO" "${GRAPHDB_REPO}" "GRAPHDB_PASSWORD" "${GRAPHDB_PASSWORD}" | \
|
|
||||||
curl -X POST $GRAPHDB_AUTH_FLAGS \
|
|
||||||
"http://127.0.0.1:7200/rest/security/users/${GRAPHDB_USER}" \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--header 'Accept: text/plain' \
|
|
||||||
-d @-
|
|
||||||
|
|
||||||
log_info " => Creating local directory structure at '$INSTANCE_BASE_DIR'"
|
|
||||||
mkdir -p "$INSTANCE_BASE_DIR"
|
|
||||||
mkdir -p "$INSTANCE_DATA_DIR"
|
|
||||||
mkdir -p "$INSTANCE_DATA_DIR/.composer"
|
|
||||||
mkdir -p "$INSTANCE_DATA_DIR/data"
|
|
||||||
touch "$INSTANCE_DATA_DIR/authorized_keys"
|
|
||||||
chmod a+rw "$INSTANCE_DATA_DIR/authorized_keys"
|
|
||||||
|
|
||||||
# Generate some more random credentials, this time for drupal.
|
|
||||||
# We again make use of the randompw alias.
|
|
||||||
log_info " => Generating new drupal credentials"
|
|
||||||
DRUPAL_USER="admin"
|
|
||||||
DRUPAL_PASS="$(randompw)"
|
|
||||||
|
|
||||||
# TODO: copy over docker-compose into the right directory
|
|
||||||
log_info " => Creating instance directory"
|
|
||||||
install_resource_dir "compose/barrel" "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
||||||
# Log all the details into the bookeeping database
|
|
||||||
log_info "=> Storing configuration in bookkeeping table"
|
|
||||||
sql_bookkeep_insert \
|
|
||||||
"slug,filesystem_base,sql_database,sql_user,sql_password,graphdb_repository,graphdb_user,graphdb_password" \
|
|
||||||
"\"${SLUG}\",\"${INSTANCE_BASE_DIR}\",\"${MYSQL_DATABASE}\",\"${MYSQL_USER}\",\"${MYSQL_PASSWORD}\",\"${GRAPHDB_REPO}\",\"${GRAPHDB_USER}\",\"${GRAPHDB_PASSWORD}\""
|
|
||||||
|
|
||||||
log_info " => Writing configuration file"
|
|
||||||
load_template "docker-env/barrel" \
|
|
||||||
"REAL_PATH" "${INSTANCE_DATA_DIR}" \
|
|
||||||
"GLOBAL_AUTHORIZED_KEYS_FILE" "${GLOBAL_AUTHORIZED_KEYS_FILE}" \
|
|
||||||
"VIRTUAL_HOST" "${INSTANCE_DOMAIN}" \
|
|
||||||
"SLUG" "${SLUG}" \
|
|
||||||
"LETSENCRYPT_HOST" "${LETSENCRYPT_HOST}" \
|
|
||||||
"LETSENCRYPT_EMAIL" "${LETSENCRYPT_EMAIL}" \
|
|
||||||
"DISTILLERY_DIR" "${DIR}" \
|
|
||||||
> "$INSTANCE_BASE_DIR/.env"
|
|
||||||
|
|
||||||
|
|
||||||
log_info " => Running and building image"
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
docker-compose build --pull
|
|
||||||
docker-compose pull
|
|
||||||
|
|
||||||
log_info " => Running provision script"
|
|
||||||
docker-compose run --rm barrel /bin/bash -c "sudo PATH=\$PATH -u www-data /bin/bash /provision_container.sh \
|
|
||||||
\"${INSTANCE_DOMAIN}\" \
|
|
||||||
\"${MYSQL_DATABASE}\" \"${MYSQL_USER}\" \"${MYSQL_PASSWORD}\" \
|
|
||||||
\"${GRAPHDB_REPO}\" \"${GRAPHDB_USER}\" \"${GRAPHDB_PASSWORD}\" \
|
|
||||||
\"${DRUPAL_USER}\" \"${DRUPAL_PASS}\" \
|
|
||||||
\"${DRUPAL_VERSION}\" \"${WISSKI_VERSION}\""
|
|
||||||
|
|
||||||
|
|
||||||
log_info " => Starting container"
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
# To install a new system:
|
|
||||||
|
|
||||||
# This script will provision a new Drupal instance and make it available to apache.
|
|
||||||
# Usage: sudo ./provision.sh $SLUG
|
|
||||||
# In case the installation fails, it will bail out and leave you with an incomplete installation.
|
|
||||||
# To delete an incomplete installation, use the ./purge.sh script, or try fixing the error manually.
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
# wait for sql to be awake
|
|
||||||
wait_for_sql
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
log_info " => I'm about to delete the '$SLUG' site from this system. "
|
|
||||||
read -p "This can not be undone. Please type 'y' to continue: " yn
|
|
||||||
case $yn in
|
|
||||||
[Yy]* ) break;;
|
|
||||||
* ) echo "Abort. "; exit 1;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# check if the site exists
|
|
||||||
if ! sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' does not exist in bookeeping table. "
|
|
||||||
echo "I'll try to cleanup with the current defaults. "
|
|
||||||
echo "This may or may not work. "
|
|
||||||
else
|
|
||||||
# Read all the configuration from the database
|
|
||||||
log_info " => Reading components from database"
|
|
||||||
read -r INSTANCE_BASE_DIR MYSQL_DATABASE MYSQL_USER GRAPHDB_REPO GRAPHDB_USER <<< "$(sql_bookkeep_load "${SLUG}" "filesystem_base,sql_database,sql_user,graphdb_repository,graphdb_user" | tail -n +2)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# stop the running system container
|
|
||||||
if [ -d "$INSTANCE_BASE_DIR" ] ; then
|
|
||||||
log_info "=> Stopping running system"
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
docker-compose down -v || true
|
|
||||||
fi;
|
|
||||||
|
|
||||||
cd
|
|
||||||
|
|
||||||
# delete the mysql database.
|
|
||||||
log_info " => Deleting MySQL database '$MYSQL_DATABASE' and user '$MYSQL_USER'. "
|
|
||||||
dockerized_mysql -e "DROP DATABASE IF EXISTS \`${MYSQL_DATABASE}\`;"
|
|
||||||
dockerized_mysql -e "DROP USER IF EXISTS \`${MYSQL_USER}\`@\`%\`;"
|
|
||||||
dockerized_mysql -e "FLUSH PRIVILEGES;"
|
|
||||||
|
|
||||||
# Clear the GraphDB repository.
|
|
||||||
log_info " => Deleting GraphDB repository '$GRAPHDB_REPO'"
|
|
||||||
curl $GRAPHDB_AUTH_FLAGS -X DELETE http://127.0.0.1:7200/rest/repositories/$GRAPHDB_REPO
|
|
||||||
|
|
||||||
log_info " => Deleting GraphDB user '$GRAPHDB_USER'"
|
|
||||||
curl $GRAPHDB_AUTH_FLAGS -X DELETE http://127.0.0.1:7200/rest/security/users/$GRAPHDB_USER
|
|
||||||
|
|
||||||
|
|
||||||
# Delete the directory
|
|
||||||
log_info " => Deleting '$INSTANCE_BASE_DIR'"
|
|
||||||
rm -rf "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
||||||
log_info " => Clearing bookkeeping record"
|
|
||||||
sql_bookeep_delete "$SLUG" || true
|
|
||||||
|
|
||||||
log_info " => '$SLUG' has been purged. "
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
|
|
||||||
# if the site doesn't exist, I can't open a shell.
|
|
||||||
if ! sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' does not exist in bookeeping table. "
|
|
||||||
echo "I can't rebuild it. "
|
|
||||||
exit 1
|
|
||||||
fi;
|
|
||||||
|
|
||||||
# Read everything from the database
|
|
||||||
read -r INSTANCE_BASE_DIR MYSQL_DATABASE MYSQL_USER GRAPHDB_REPO GRAPHDB_USER <<< "$(sql_bookkeep_load "${SLUG}" "filesystem_base,sql_database,sql_user,graphdb_repository,graphdb_user" | tail -n +2)"
|
|
||||||
|
|
||||||
log_info " => Touching authorized_keys file"
|
|
||||||
touch "$INSTANCE_BASE_DIR/data/authorized_keys"
|
|
||||||
chmod a+rw "$INSTANCE_BASE_DIR/data/authorized_keys"
|
|
||||||
|
|
||||||
log_info " => Updating compose files"
|
|
||||||
install_resource_dir "compose/barrel" "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
||||||
log_info "=> Rebuilding and restarting '$INSTANCE_BASE_DIR'"
|
|
||||||
update_stack "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
DISABLE_LOG=0
|
|
||||||
|
|
||||||
# update all the instances
|
|
||||||
for slug in $(sql_bookkeep_list); do
|
|
||||||
log_info "=> /bin/bash $DIR/rebuild.sh '$slug'"
|
|
||||||
/bin/bash "$DIR/rebuild.sh" "$slug";
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
# wait for sql to be awake
|
|
||||||
wait_for_sql
|
|
||||||
|
|
||||||
# check if the site exists
|
|
||||||
if sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' already exists in bookeeping table. "
|
|
||||||
echo "Refusing to work"
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info " => Creating local directory structure at '$INSTANCE_BASE_DIR'"
|
|
||||||
mkdir -p "$INSTANCE_BASE_DIR"
|
|
||||||
install_resource_dir "compose/reserve" "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
||||||
log_info " => Writing configuration file"
|
|
||||||
load_template "docker-env/reserve" \
|
|
||||||
"VIRTUAL_HOST" "${INSTANCE_DOMAIN}" \
|
|
||||||
"SLUG" "${SLUG}" \
|
|
||||||
"LETSENCRYPT_HOST" "${LETSENCRYPT_HOST}" \
|
|
||||||
"LETSENCRYPT_EMAIL" "${LETSENCRYPT_EMAIL}" \
|
|
||||||
> "$INSTANCE_BASE_DIR/.env"
|
|
||||||
|
|
||||||
|
|
||||||
log_info " => Running and building image"
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
docker-compose build --pull
|
|
||||||
docker-compose pull
|
|
||||||
|
|
||||||
log_info " => Starting container"
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
log_info " => $INSTANCE_DOMAIN has been reserved"
|
|
||||||
120
distillery/resources.go
Normal file
120
distillery/resources.go
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// TODO: Rename this to resources oncen finished
|
||||||
|
package distillery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resourceEmbed contains all the resources required by the WissKI-Distillery package.
|
||||||
|
//go:embed all:resources
|
||||||
|
var resourceEmbed embed.FS
|
||||||
|
|
||||||
|
// InstallResource install a resource src into dest.
|
||||||
|
// When it encounters a directory, recursively installs the directory is called.
|
||||||
|
// For each installation item, onInstallFile is called, unless onInstallFile is nil.
|
||||||
|
//
|
||||||
|
// If src points to a file, dst must either be an existing file, or not exist.
|
||||||
|
// If src points to a directory, dst must either be an existing directory, or not exist.
|
||||||
|
func InstallResource(dst, src string, onInstallFile func(dst, src string)) error {
|
||||||
|
return installFile(dst, resourceEmbed, src, onInstallFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errExpectedFileButGotDirectory = errors.New("Expected a file, but got a directory")
|
||||||
|
var errExpectedDirectoryButGotFile = errors.New("Expected a directory, but got a file")
|
||||||
|
|
||||||
|
func installFile(dst string, fsys embed.FS, src string, onInstallFile func(dst, src string)) error {
|
||||||
|
// call the on-install file path
|
||||||
|
if onInstallFile != nil {
|
||||||
|
onInstallFile(dst, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the source file!
|
||||||
|
srcFile, err := fsys.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Error opening source file %s", src)
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// stat the source file to install
|
||||||
|
srcStat, srcErr := srcFile.Stat()
|
||||||
|
if srcErr != nil {
|
||||||
|
return errors.Wrapf(srcErr, "Error calling stat on source %s", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it is a directory, we should recurse!
|
||||||
|
if srcStat.IsDir() {
|
||||||
|
return installDir(dst, srcStat, srcFile, fsys, src, onInstallFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if we need to create the destination file, or if it already exists
|
||||||
|
dstStat, dstErr := os.Stat(dst)
|
||||||
|
flag := os.O_WRONLY
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(dstErr):
|
||||||
|
flag |= os.O_CREATE
|
||||||
|
case dstErr != nil:
|
||||||
|
return errors.Wrapf(dstErr, "Error calling stat on destination %s", dst)
|
||||||
|
case dstStat.IsDir():
|
||||||
|
return errors.Wrapf(errExpectedFileButGotDirectory, "Error processing destination %s", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the file
|
||||||
|
dstFile, err := os.OpenFile(dst, flag, srcStat.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Error opening destination %s", dst)
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
// copy over the content
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
return errors.Wrapf(err, "Error writing to destination %s", dst)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func installDir(dst string, srcStat fs.FileInfo, srcFile fs.File, fsys embed.FS, src string, onInstallFile func(dst, src string)) error {
|
||||||
|
// make sure it is a directory!
|
||||||
|
dir, ok := srcFile.(fs.ReadDirFile)
|
||||||
|
if !ok {
|
||||||
|
return errExpectedDirectoryButGotFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the destination
|
||||||
|
dstStat, dstErr := os.Stat(dst)
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(dstErr):
|
||||||
|
if err := os.MkdirAll(dst, srcStat.Mode()); err != nil {
|
||||||
|
return errors.Wrapf(err, "Error creating destination directory %s", dst)
|
||||||
|
}
|
||||||
|
case dstErr != nil:
|
||||||
|
return errors.Wrapf(dstErr, "Error calling stat on destination %s", dst)
|
||||||
|
case !dstStat.IsDir():
|
||||||
|
return errors.Wrapf(errExpectedDirectoryButGotFile, "Error opening destination %s", dst)
|
||||||
|
case dstErr == nil:
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the directory
|
||||||
|
entries, err := dir.ReadDir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Error reading source directory %s", srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over all the children
|
||||||
|
for _, entry := range entries {
|
||||||
|
if err := func(dst, src string) error {
|
||||||
|
return installFile(dst, fsys, src, onInstallFile)
|
||||||
|
}(
|
||||||
|
filepath.Join(dst, entry.Name()),
|
||||||
|
filepath.Join(src, entry.Name()),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
#######################
|
#######################
|
||||||
|
|
||||||
# Real path for volumes to be stored
|
# Real path for volumes to be stored
|
||||||
REAL_PATH=/var/www/example.slug
|
REAL_PATH=/var/www/deploy/instances/example.slug
|
||||||
DISTILLERY_DIR=/distillery/
|
UTILS_DIR=/var/www/deploy/runtime/utils/
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
### Web Server settings
|
### Web Server settings
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ 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 [ "/entrypoint.sh" ]
|
ENTRYPOINT [ "/bin/bash", "/entrypoint.sh" ]
|
||||||
CMD ["apache2-foreground"]
|
CMD ["apache2-foreground"]
|
||||||
|
|
||||||
# Add the provision script and WissKI utils
|
# Add the provision script and WissKI utils
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ services:
|
||||||
- ${REAL_PATH}/.composer:/var/www/.composer
|
- ${REAL_PATH}/.composer:/var/www/.composer
|
||||||
- ${REAL_PATH}/data:/var/www/data
|
- ${REAL_PATH}/data:/var/www/data
|
||||||
- ${REAL_PATH}/authorized_keys:/var/www/.ssh/authorized_keys
|
- ${REAL_PATH}/authorized_keys:/var/www/.ssh/authorized_keys
|
||||||
- ${DISTILLERY_DIR}/utils:/utils:ro
|
- ${UTILS_DIR}:/utils:ro
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -170,3 +170,5 @@ function printdetails() {
|
||||||
echo "Password: $DRUPAL_PASS"
|
echo "Password: $DRUPAL_PASS"
|
||||||
}
|
}
|
||||||
printdetails
|
printdetails
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#######################
|
|
||||||
# Meta Settings
|
|
||||||
#######################
|
|
||||||
# The target path to redirect to
|
|
||||||
TARGET=https://github.com/FAU-CDI/wisski-distillery
|
|
||||||
|
|
||||||
# path to .json
|
|
||||||
SELF_OVERRIDES_FILE=/overrides.json
|
|
||||||
|
|
||||||
#######################
|
|
||||||
### Web Server settings
|
|
||||||
#######################
|
|
||||||
# the hostname for the website
|
|
||||||
VIRTUAL_HOST=example.com
|
|
||||||
|
|
||||||
# optional letsencrypt support
|
|
||||||
# when blank, ignore
|
|
||||||
LETSENCRYPT_HOST=
|
|
||||||
LETSENCRYPT_EMAIL=
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# Several docker-compose files are created to manage global services and the system itself.
|
# Several docker-compose files are created to manage global services and the system itself.
|
||||||
# On top of this all real-system space will be created under this directory.
|
# On top of this all real-system space will be created under this directory.
|
||||||
DEPLOY_ROOT=/var/www/deploy
|
DEPLOY_ROOT=${DEPLOY_ROOT}
|
||||||
|
|
||||||
# Each created Drupal Instance corresponds to a single domain name.
|
# Each created Drupal Instance corresponds to a single domain name.
|
||||||
# These domain names should either be a complete domain name or a sub-domain of a default domain.
|
# These domain names should either be a complete domain name or a sub-domain of a default domain.
|
||||||
# This setting configures the default domain-name to create subdomains of.
|
# This setting configures the default domain-name to create subdomains of.
|
||||||
DEFAULT_DOMAIN=localhost.kwarc.info
|
DEFAULT_DOMAIN=${DEFAULT_DOMAIN}
|
||||||
|
|
||||||
# By default, the default domain redirects to the distillery repository.
|
# By default, the default domain redirects to the distillery repository.
|
||||||
# If you want to change this, set an alternate domain name here.
|
# If you want to change this, set an alternate domain name here.
|
||||||
|
|
@ -17,7 +17,7 @@ SELF_EXTRA_DOMAINS=
|
||||||
|
|
||||||
# You can override individual URLS in the homepage.
|
# You can override individual URLS in the homepage.
|
||||||
# Do this by adding URLs (without trailing '/'s) into a JSON file
|
# Do this by adding URLs (without trailing '/'s) into a JSON file
|
||||||
SELF_OVERRIDES_FILE=/distillery/overrides.json
|
SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
|
||||||
|
|
||||||
# The system can support setting up certificate(s) automatically.
|
# The system can support setting up certificate(s) automatically.
|
||||||
# It can be enabled by setting an email for certbot certificates.
|
# It can be enabled by setting an email for certbot certificates.
|
||||||
|
|
@ -52,7 +52,12 @@ DISTILLERY_BOOKKEEPING_TABLE=distillery
|
||||||
PASSWORD_LENGTH=64
|
PASSWORD_LENGTH=64
|
||||||
|
|
||||||
# A file to be used for global authorized_keys for the ssh server.
|
# A file to be used for global authorized_keys for the ssh server.
|
||||||
GLOBAL_AUTHORIZED_KEYS_FILE=/distillery/authorized_keys
|
GLOBAL_AUTHORIZED_KEYS_FILE=${AUTHORIZED_KEYS_FILE}
|
||||||
|
|
||||||
# The admin password of the GraphDB interface, to be used for queries
|
# The admin user and password of the GraphDB interface, to be used for queries
|
||||||
GRAPHDB_ADMIN_PASSWORD=root
|
GRAPHDB_ADMIN_USER=${GRAPHDB_ADMIN_USER}
|
||||||
|
GRAPHDB_ADMIN_PASSWORD=${GRAPHDB_ADMIN_PASSWORD}
|
||||||
|
|
||||||
|
# The admin password to use for access to mysql
|
||||||
|
MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER}
|
||||||
|
MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# This file contains authorized_keys files valid for every repository in the distillery.
|
||||||
|
# To add a key, add one file per line.
|
||||||
1
distillery/resources/templates/bootstrap/overrides.json
Normal file
1
distillery/resources/templates/bootstrap/overrides.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
REAL_PATH=${REAL_PATH}
|
REAL_PATH=${REAL_PATH}
|
||||||
DISTILLERY_DIR=${DISTILLERY_DIR}
|
UTILS_DIR=${UTILS_DIR}
|
||||||
|
|
||||||
SLUG=${SLUG}
|
SLUG=${SLUG}
|
||||||
VIRTUAL_HOST=${VIRTUAL_HOST}
|
VIRTUAL_HOST=${VIRTUAL_HOST}
|
||||||
|
|
|
||||||
112
distillery/resources_template.go
Normal file
112
distillery/resources_template.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package distillery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
var templateRegexp = regexp.MustCompile(`\${[^}]+}`)
|
||||||
|
|
||||||
|
// InstallTemplates open the resource src, and installs it into dst.
|
||||||
|
// the template resource must fit into memory.
|
||||||
|
//
|
||||||
|
// For each variable ${THING} inside dest, a key 'THING' must exist in context.
|
||||||
|
// Extra or missing template keys are an error.
|
||||||
|
func InstallTemplate(dst, src string, context map[string]string) error {
|
||||||
|
bytes, srcMode, err := doTemplate(src, context)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if we need to create the destination file, or if it already exists
|
||||||
|
dstStat, dstErr := os.Stat(dst)
|
||||||
|
flag := os.O_WRONLY
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(dstErr):
|
||||||
|
flag |= os.O_CREATE
|
||||||
|
case dstErr != nil:
|
||||||
|
return errors.Wrapf(dstErr, "Error calling stat on destination %s", dst)
|
||||||
|
case dstStat.IsDir():
|
||||||
|
return errors.Wrapf(errExpectedFileButGotDirectory, "Error processing destination %s", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open and write the destination file
|
||||||
|
dstFile, err := os.OpenFile(dst, flag, srcMode)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Unable to open file %s", dst)
|
||||||
|
}
|
||||||
|
_, err = dstFile.Write(bytes)
|
||||||
|
return errors.Wrapf(err, "Unable to write destination %s", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadTemplate is like InstallTemplate, except that it writes template into a byte slice and returns it.
|
||||||
|
func ReadTemplate(src string, context map[string]string) ([]byte, error) {
|
||||||
|
bytes, _, err := doTemplate(src, context)
|
||||||
|
return bytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTemplate(src string, context map[string]string) (bytes []byte, mode fs.FileMode, err error) {
|
||||||
|
// open the source file!
|
||||||
|
srcFile, err := resourceEmbed.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mode, errors.Wrapf(err, "Error opening source file %s", src)
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// stat the source file to install
|
||||||
|
srcStat, srcErr := srcFile.Stat()
|
||||||
|
if srcErr != nil {
|
||||||
|
return nil, mode, errors.Wrapf(srcErr, "Error calling stat on source %s", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not be a directory
|
||||||
|
if srcStat.IsDir() {
|
||||||
|
return nil, mode, errors.Wrapf(errExpectedFileButGotDirectory, "Error calling stat on source %s", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the template and replace
|
||||||
|
templates, err := io.ReadAll(srcFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, mode, errors.Wrapf(err, "Unable to read src file %s", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep track of context keys that have not been used
|
||||||
|
unuusedContext := make(map[string]struct{}, len(context))
|
||||||
|
for key := range context {
|
||||||
|
unuusedContext[key] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the template regexp
|
||||||
|
// keeping track of unuused errors
|
||||||
|
var hadError error
|
||||||
|
templates = templateRegexp.ReplaceAllFunc(templates, func(b []byte) []byte {
|
||||||
|
name := string(b[2 : len(b)-1]) // remove the leading ${ and trailing }
|
||||||
|
delete(unuusedContext, name) // mark the key as having been read
|
||||||
|
|
||||||
|
value, ok := context[name]
|
||||||
|
if hadError != nil && !ok {
|
||||||
|
hadError = errors.Errorf("key %s missing in context", name)
|
||||||
|
}
|
||||||
|
return []byte(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if hadError != nil {
|
||||||
|
return nil, mode, hadError
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unuusedContext) != 0 {
|
||||||
|
keys := maps.Keys(unuusedContext)
|
||||||
|
slices.Sort(keys)
|
||||||
|
return nil, mode, errors.Errorf("additional keys %s in context", strings.Join(keys, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the data and the mode!
|
||||||
|
return templates, srcStat.Mode(), nil
|
||||||
|
}
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh and read the slug argument.
|
|
||||||
DISABLE_LOG=1
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
DISABLE_LOG=0
|
|
||||||
require_slug_argument
|
|
||||||
|
|
||||||
|
|
||||||
# if the site doesn't exist, I can't open a shell.
|
|
||||||
if ! sql_bookkeep_exists "$SLUG"; then
|
|
||||||
log_error "=> Site '$SLUG' does not exist in bookeeping table. "
|
|
||||||
echo "I can't open a shell there. "
|
|
||||||
exit 1
|
|
||||||
fi;
|
|
||||||
|
|
||||||
# Read everything from the database
|
|
||||||
read -r INSTANCE_BASE_DIR MYSQL_DATABASE MYSQL_USER GRAPHDB_REPO GRAPHDB_USER <<< "$(sql_bookkeep_load "${SLUG}" "filesystem_base,sql_database,sql_user,graphdb_repository,graphdb_user" | tail -n +2)"
|
|
||||||
|
|
||||||
# cd into the right directory
|
|
||||||
cd "$INSTANCE_BASE_DIR"
|
|
||||||
|
|
||||||
# and open a www-data shell
|
|
||||||
docker-compose exec barrel /user_shell.sh
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
|
|
||||||
# Read the 'GRAPHDB_ZIP' argument from the command line.
|
|
||||||
# If it's not set, throw an error.
|
|
||||||
GRAPHDB_ZIP=$1
|
|
||||||
if [ -z "$GRAPHDB_ZIP" ]; then
|
|
||||||
log_error "Usage: system_install.sh GRAPHDB_ZIP"
|
|
||||||
exit 1;
|
|
||||||
fi;
|
|
||||||
|
|
||||||
|
|
||||||
# print some general info on the screen
|
|
||||||
log_info "=> Preparing system to become a WissKI Distillery"
|
|
||||||
echo "This script will install or upgrade this system to become a WissKI distillery. "
|
|
||||||
echo "It is idempotent and can safely be run multiple times. "
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
|
|
||||||
# Install default system upgrades.
|
|
||||||
log_info "=> Installing system updates"
|
|
||||||
apt-get update
|
|
||||||
apt-get upgrade -y
|
|
||||||
|
|
||||||
# install docker dependencies.
|
|
||||||
log_info "=> Installing docker installer dependencies"
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y curl
|
|
||||||
|
|
||||||
# install docker using an automated script.
|
|
||||||
log_info "=> Installing docker"
|
|
||||||
curl -fsSL https://get.docker.com -o - | /bin/sh
|
|
||||||
|
|
||||||
# install docker-compose dependencies.
|
|
||||||
log_info "=> Install docker-compose installer dependencies"
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y python3-pip libffi-dev
|
|
||||||
|
|
||||||
# install docker-compose.
|
|
||||||
log_info "=> Installing docker-compose"
|
|
||||||
pip3 install --upgrade docker-compose
|
|
||||||
|
|
||||||
log_info "=> Creating docker-compose directories and files"
|
|
||||||
mkdir -p "$DEPLOY_INSTANCES_DIR"
|
|
||||||
mkdir -p "$DEPLOY_WEB_DIR"
|
|
||||||
mkdir -p "$DEPLOY_SELF_DIR"
|
|
||||||
mkdir -p "$DEPLOY_RESOLVER_DIR"
|
|
||||||
mkdir -p "$DEPLOY_SSH_DIR"
|
|
||||||
mkdir -p "$DEPLOY_TRIPLESTORE_DIR"
|
|
||||||
mkdir -p "$DEPLOY_SQL_DIR"
|
|
||||||
mkdir -p "$DEPLOY_BACKUP_INPROGRESS_DIR"
|
|
||||||
mkdir -p "$DEPLOY_BACKUP_FINAL_DIR"
|
|
||||||
|
|
||||||
log_info "=> Creating 'distillery' network"
|
|
||||||
docker network create distillery || true
|
|
||||||
|
|
||||||
log_info "=> Creating 'docker-compose' files for the 'web'. "
|
|
||||||
install_resource_dir "compose/web" "$DEPLOY_WEB_DIR"
|
|
||||||
|
|
||||||
log_info " => Writing 'web' configuration file"
|
|
||||||
load_template "docker-env/web" \
|
|
||||||
"DEFAULT_HOST" "${DEFAULT_DOMAIN}" \
|
|
||||||
> "$DEPLOY_WEB_DIR/.env"
|
|
||||||
|
|
||||||
log_info "=> Creating 'docker-compose' files for the 'self'. "
|
|
||||||
install_resource_dir "compose/self" "$DEPLOY_SELF_DIR"
|
|
||||||
|
|
||||||
log_info "=> Creating 'docker-compose' files for the 'resolver'. "
|
|
||||||
install_resource_dir "compose/resolver" "$DEPLOY_RESOLVER_DIR"
|
|
||||||
touch "$DEPLOY_PREFIX_CONFIG"
|
|
||||||
|
|
||||||
log_info "=> Creating 'docker-compose' files for the 'ssh'. "
|
|
||||||
install_resource_dir "compose/ssh" "$DEPLOY_SSH_DIR"
|
|
||||||
|
|
||||||
# setup the lesencrypt host for the default domain
|
|
||||||
if [ -n "$LETSENCRYPT_HOST" ]; then
|
|
||||||
LETSENCRYPT_HOST="$SELF_DOMAIN_SPEC"
|
|
||||||
fi;
|
|
||||||
|
|
||||||
log_info " => Writing 'self' configuration file"
|
|
||||||
load_template "docker-env/self" \
|
|
||||||
"VIRTUAL_HOST" "${SELF_DOMAIN_SPEC}" \
|
|
||||||
"LETSENCRYPT_HOST" "${LETSENCRYPT_HOST}" \
|
|
||||||
"LETSENCRYPT_EMAIL" "${LETSENCRYPT_EMAIL}" \
|
|
||||||
"TARGET" "${SELF_REDIRECT}" \
|
|
||||||
"OVERRIDES_FILE" "${SELF_OVERRIDES_FILE}" \
|
|
||||||
> "$DEPLOY_SELF_DIR/.env"
|
|
||||||
|
|
||||||
log_info " => Writing 'resolver' configuration file"
|
|
||||||
load_template "docker-env/resolver" \
|
|
||||||
"VIRTUAL_HOST" "${SELF_DOMAIN_SPEC}" \
|
|
||||||
"LETSENCRYPT_HOST" "${LETSENCRYPT_HOST}" \
|
|
||||||
"LETSENCRYPT_EMAIL" "${LETSENCRYPT_EMAIL}" \
|
|
||||||
"PREFIX_FILE" "${DEPLOY_PREFIX_CONFIG}" \
|
|
||||||
"DEFAULT_DOMAIN" "${DEFAULT_DOMAIN}" \
|
|
||||||
"LEGACY_DOMAIN" "${SELF_EXTRA_DOMAINS}" \
|
|
||||||
> "$DEPLOY_RESOLVER_DIR/.env"
|
|
||||||
|
|
||||||
# copy over the directory
|
|
||||||
log_info "=> Creating 'docker-compose' files for the 'triplestore'. "
|
|
||||||
install_resource_dir "compose/triplestore" "$DEPLOY_TRIPLESTORE_DIR"
|
|
||||||
|
|
||||||
# copy the graphdb.zip
|
|
||||||
echo "Writing \"$DEPLOY_TRIPLESTORE_DIR/graphdb.zip\""
|
|
||||||
cp "$GRAPHDB_ZIP" "$DEPLOY_TRIPLESTORE_DIR/graphdb.zip"
|
|
||||||
|
|
||||||
# create data (volume) location
|
|
||||||
mkdir -p "$DEPLOY_TRIPLESTORE_DIR/data/data/"
|
|
||||||
mkdir -p "$DEPLOY_TRIPLESTORE_DIR/data/work/"
|
|
||||||
mkdir -p "$DEPLOY_TRIPLESTORE_DIR/data/logs/"
|
|
||||||
|
|
||||||
# copy over the sql resource directory, then ensure the data diretory for sql exists.
|
|
||||||
log_info "=> Creating 'docker-compose' files for the 'sql'. "
|
|
||||||
install_resource_dir "compose/sql" "$DEPLOY_SQL_DIR"
|
|
||||||
mkdir -p "$DEPLOY_SQL_DIR/data/"
|
|
||||||
|
|
||||||
# Run all the updates via system_update.sh
|
|
||||||
log_info " => Running 'system_update.sh'"
|
|
||||||
bash "$SCRIPT_DIR/system_update.sh"
|
|
||||||
|
|
||||||
log_info "=> Waiting for sql to come up"
|
|
||||||
wait_for_sql
|
|
||||||
|
|
||||||
log_info "=> Creating '$DISTILLERY_BOOKKEEPING_DATABASE' database and '$DISTILLERY_BOOKKEEPING_TABLE' table"
|
|
||||||
load_template "bookkeeping/create.sql" "DATABASE" "$DISTILLERY_BOOKKEEPING_DATABASE" "TABLE" "$DISTILLERY_BOOKKEEPING_TABLE" | \
|
|
||||||
dockerized_mysql
|
|
||||||
|
|
||||||
log_info "=> System installation finished, ready to distill. "
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# read the lib/shared.sh
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
cd "$DIR"
|
|
||||||
source "$DIR/lib/lib.sh"
|
|
||||||
|
|
||||||
log_info "=> Rebuilding and restarting 'web' stack"
|
|
||||||
update_stack "$DEPLOY_WEB_DIR"
|
|
||||||
|
|
||||||
log_info "=> Rebuilding and restarting 'self' stack"
|
|
||||||
update_stack "$DEPLOY_SELF_DIR"
|
|
||||||
|
|
||||||
# build and start the ssh server
|
|
||||||
log_info "=> Rebuilding and restarting 'ssh' stack"
|
|
||||||
update_stack "$DEPLOY_SSH_DIR"
|
|
||||||
|
|
||||||
# build and start the triplestore
|
|
||||||
log_info "=> Rebuilding and restarting 'triplestore' stack"
|
|
||||||
update_stack "$DEPLOY_TRIPLESTORE_DIR"
|
|
||||||
|
|
||||||
# build and start the triplestore
|
|
||||||
log_info "=> Rebuilding and restarting 'sql' stack"
|
|
||||||
update_stack "$DEPLOY_SQL_DIR"
|
|
||||||
|
|
||||||
log_info " => Updating Prefix Config"
|
|
||||||
cd "$DIR"
|
|
||||||
bash update_prefix_config.sh
|
|
||||||
|
|
||||||
log_info "=> Rebuilding and restarting 'resolver' stack"
|
|
||||||
update_stack "$DEPLOY_RESOLVER_DIR"
|
|
||||||
|
|
||||||
log_info "=> System up-to-date. "
|
|
||||||
23
env/dirs.go
vendored
Normal file
23
env/dirs.go
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
func (dis Distillery) BackupDir() string {
|
||||||
|
return filepath.Join(dis.Config.DeployRoot, "backups")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) RuntimeDir() string {
|
||||||
|
return filepath.Join(dis.Config.DeployRoot, "runtime")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) RuntimeUtilsDir() string {
|
||||||
|
return filepath.Join(dis.Config.DeployRoot, "runtime", "utils")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) InprogressBackupPath() string {
|
||||||
|
return filepath.Join(dis.BackupDir(), "inprogress")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) FinalBackupPath() string {
|
||||||
|
return filepath.Join(dis.BackupDir(), "final")
|
||||||
|
}
|
||||||
77
env/distillery.go
vendored
Normal file
77
env/distillery.go
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/config"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Distillery represents a running instance for the distillery
|
||||||
|
type Distillery struct {
|
||||||
|
Config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) HTTPSEnabled() bool {
|
||||||
|
return dis.Config.CertbotEmail != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the default virtual host
|
||||||
|
func (dis Distillery) DefaultVirtualHost() string {
|
||||||
|
VIRTUAL_HOST := dis.Config.DefaultDomain
|
||||||
|
if len(dis.Config.SelfExtraDomains) > 0 {
|
||||||
|
VIRTUAL_HOST += "," + strings.Join(dis.Config.SelfExtraDomains, ",")
|
||||||
|
}
|
||||||
|
return VIRTUAL_HOST
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) DefaultLetsencryptHost() string {
|
||||||
|
if !dis.HTTPSEnabled() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dis.DefaultVirtualHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context returns a new Context belonging to this distillery
|
||||||
|
func (dis Distillery) Context() context.Context {
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNoConfigFile = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneralArguments,
|
||||||
|
Message: "Configuration File does not exist",
|
||||||
|
}
|
||||||
|
|
||||||
|
var errOpenConfig = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneralArguments,
|
||||||
|
Message: "error loading configuration file: %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDistillery creates a new distillery object from a set of parameters and requirements
|
||||||
|
func NewDistillery(params Params, req Requirements) (env *Distillery, err error) {
|
||||||
|
env = &Distillery{}
|
||||||
|
|
||||||
|
// if we don't need to load the config, there is nothing to do
|
||||||
|
if !req.NeedsConfig {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no no config file, return
|
||||||
|
cfg := params.ConfigFilePath()
|
||||||
|
if cfg == "" {
|
||||||
|
return nil, errNoConfigFile
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(params.ConfigFilePath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errOpenConfig.WithMessageF(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// unmarshal the config
|
||||||
|
env.Config = &config.Config{}
|
||||||
|
err = env.Config.Unmarshal(f)
|
||||||
|
return
|
||||||
|
}
|
||||||
359
env/instances.go
vendored
Normal file
359
env/instances.go
vendored
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/alessio/shellescape"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errNoBookkeeping = exit.Error{
|
||||||
|
Message: "instance %q does not exist in bookkeeping table",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrInstanceNotFound = exit.Error{
|
||||||
|
Message: "instance not found",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance returns the instance of the WissKI Distillery with the provided slug
|
||||||
|
func (dis *Distillery) Instance(slug string) (i Instance, err error) {
|
||||||
|
if err := dis.SQLWaitForConnection(); err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := dis.sqlBkTable(false)
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the instance by slug
|
||||||
|
query := table.Where(&bookkeeping.Instance{Slug: slug}).Find(&i.Instance)
|
||||||
|
switch {
|
||||||
|
case query.Error != nil:
|
||||||
|
return i, errSQL.WithMessageF(query.Error)
|
||||||
|
case query.RowsAffected == 0:
|
||||||
|
return i, ErrInstanceNotFound
|
||||||
|
default:
|
||||||
|
i.dis = dis
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasInstance checks if the provided instance exists in the bookeeping table
|
||||||
|
func (dis *Distillery) HasInstance(slug string) (ok bool, err error) {
|
||||||
|
if err := dis.SQLWaitForConnection(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := dis.sqlBkTable(false)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := table.Select("count(*) > 0").Where("slug = ?", slug).Find(&ok)
|
||||||
|
if query.Error != nil {
|
||||||
|
return false, errSQL.WithMessageF(query.Error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instances is like InstancesWith, except that when no slugs are provided, it calls AllInstances.
|
||||||
|
func (dis *Distillery) Instances(slugs ...string) ([]Instance, error) {
|
||||||
|
if len(slugs) == 0 {
|
||||||
|
return dis.AllInstances()
|
||||||
|
}
|
||||||
|
return dis.InstancesWith(slugs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllInstances returns all instances of the WissKI Distillery in consistent order.
|
||||||
|
//
|
||||||
|
// There is no guarantee that this order remains identical between different api releases; however subsequent invocations are guaranteed to return the same order.
|
||||||
|
func (dis *Distillery) AllInstances() ([]Instance, error) {
|
||||||
|
return dis.findInstances(true, func(table *gorm.DB) *gorm.DB {
|
||||||
|
return table
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstancesWith returns all instances where the slug is in the provided list of names.
|
||||||
|
// The returned instances are reordered in a consistent order.
|
||||||
|
func (dis *Distillery) InstancesWith(slugs ...string) ([]Instance, error) {
|
||||||
|
return dis.findInstances(true, func(table *gorm.DB) *gorm.DB {
|
||||||
|
return table.Where("slug IN ?", slugs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// findInstances finds instance objects based on a query in the bookkeeping table
|
||||||
|
func (dis *Distillery) findInstances(order bool, query func(table *gorm.DB) *gorm.DB) (instances []Instance, err error) {
|
||||||
|
if err := dis.SQLWaitForConnection(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the bookkeeping table
|
||||||
|
table, err := dis.sqlBkTable(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare a query
|
||||||
|
find := table
|
||||||
|
if order {
|
||||||
|
find = find.Order(clause.OrderByColumn{Column: clause.Column{Name: "slug"}, Desc: false})
|
||||||
|
}
|
||||||
|
if query != nil {
|
||||||
|
find = query(find)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch bookkeeping instances
|
||||||
|
var bks []bookkeeping.Instance
|
||||||
|
find = find.Find(&bks)
|
||||||
|
if find.Error != nil {
|
||||||
|
return nil, errSQL.WithMessageF(find.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make proper instances
|
||||||
|
instances = make([]Instance, len(bks))
|
||||||
|
for i, bk := range bks {
|
||||||
|
instances[i].Instance = bk
|
||||||
|
instances[i].dis = dis
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance represents a bookkeeping instance
|
||||||
|
type Instance struct {
|
||||||
|
bookkeeping.Instance
|
||||||
|
|
||||||
|
// Credentials for the drupal instance
|
||||||
|
DrupalUsername string
|
||||||
|
DrupalPassword string
|
||||||
|
|
||||||
|
dis *Distillery
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the bookkeeping table with this instance.
|
||||||
|
func (instance *Instance) Update() error {
|
||||||
|
db, err := instance.dis.sqlBkTable(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// it has never been created => we need to create it in the database
|
||||||
|
if instance.Instance.Created.IsZero() {
|
||||||
|
return db.Create(&instance.Instance).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update based on the primary key!
|
||||||
|
return db.Where("pk = ?", instance.Instance.Pk).Updates(&instance.Instance).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes this instance from the bookkeeping table
|
||||||
|
func (instance *Instance) Delete() error {
|
||||||
|
db, err := instance.dis.sqlBkTable(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't exist => nothing to delete
|
||||||
|
if instance.Instance.Created.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete it directly
|
||||||
|
return db.Delete(&instance.Instance).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell executes a shell command inside the
|
||||||
|
func (instance Instance) Shell(io stream.IOStream, argv ...string) int {
|
||||||
|
return instance.Stack().Exec(io, "barrel", "/user_shell.sh", argv...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain returns the full domain name of this instance
|
||||||
|
func (instance Instance) Domain() string {
|
||||||
|
return fmt.Sprintf("%s.%s", instance.Slug, instance.dis.Config.DefaultDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IfHttps returns value if the distillery has https enabled, the empty string otherwise
|
||||||
|
// TODO: Fix this to be in a proper place
|
||||||
|
func (dis *Distillery) IfHttps(value string) string {
|
||||||
|
if !dis.HTTPSEnabled() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL returns the public URL of this instance
|
||||||
|
func (instance Instance) URL() *url.URL {
|
||||||
|
// setup domain and path
|
||||||
|
url := &url.URL{
|
||||||
|
Host: instance.Domain(),
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
|
||||||
|
// use http or https scheme depending on if the distillery has it enabled
|
||||||
|
if instance.dis.HTTPSEnabled() {
|
||||||
|
url.Scheme = "https"
|
||||||
|
} else {
|
||||||
|
url.Scheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack represents a stack representing this instance
|
||||||
|
func (instance Instance) Stack() stack.Installable {
|
||||||
|
return stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "barrel",
|
||||||
|
Dir: instance.FilesystemBase,
|
||||||
|
},
|
||||||
|
ContextResource: filepath.Join("resources", "compose", "barrel"),
|
||||||
|
|
||||||
|
EnvFileResource: filepath.Join("resources", "templates", "docker-env", "barrel"),
|
||||||
|
EnvFileContext: map[string]string{
|
||||||
|
"REAL_PATH": instance.FilesystemBase,
|
||||||
|
|
||||||
|
"SLUG": instance.Slug,
|
||||||
|
"VIRTUAL_HOST": instance.Domain(),
|
||||||
|
|
||||||
|
"LETSENCRYPT_HOST": instance.dis.IfHttps(instance.Domain()),
|
||||||
|
"LETSENCRYPT_EMAIL": instance.dis.IfHttps(instance.dis.Config.CertbotEmail),
|
||||||
|
|
||||||
|
"UTILS_DIR": instance.dis.RuntimeUtilsDir(),
|
||||||
|
"GLOBAL_AUTHORIZED_KEYS_FILE": instance.dis.Config.GlobalAuthorizedKeysFile,
|
||||||
|
},
|
||||||
|
|
||||||
|
CopyContextFiles: nil,
|
||||||
|
|
||||||
|
TouchFiles: []string{
|
||||||
|
"authorized_keys",
|
||||||
|
},
|
||||||
|
|
||||||
|
MakeDirsPerm: fs.ModeDir | fs.ModePerm,
|
||||||
|
MakeDirs: []string{"data", ".composer"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (instance Instance) ReserveStack() stack.Installable {
|
||||||
|
return stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "reserve",
|
||||||
|
Dir: instance.FilesystemBase,
|
||||||
|
},
|
||||||
|
ContextResource: filepath.Join("resources", "compose", "reserve"),
|
||||||
|
|
||||||
|
EnvFileResource: filepath.Join("resources", "templates", "docker-env", "reserve"),
|
||||||
|
EnvFileContext: map[string]string{
|
||||||
|
"VIRTUAL_HOST": instance.Domain(),
|
||||||
|
|
||||||
|
"LETSENCRYPT_HOST": instance.dis.IfHttps(instance.Domain()),
|
||||||
|
"LETSENCRYPT_EMAIL": instance.dis.IfHttps(instance.dis.Config.CertbotEmail),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision provisions an instance, assuming that the required databases already exist.
|
||||||
|
func (instance Instance) Provision(io stream.IOStream) error {
|
||||||
|
|
||||||
|
// create the basic st!
|
||||||
|
st := instance.Stack()
|
||||||
|
if err := st.Install(io, stack.InstallationContext{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull and build the stack!
|
||||||
|
if err := st.Update(io, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
provisionParams := []string{
|
||||||
|
instance.Domain(),
|
||||||
|
|
||||||
|
instance.SqlDatabase,
|
||||||
|
instance.SqlUser,
|
||||||
|
instance.SqlPassword,
|
||||||
|
|
||||||
|
instance.GraphDBRepository,
|
||||||
|
instance.GraphDBUser,
|
||||||
|
instance.GraphDBPassword,
|
||||||
|
|
||||||
|
instance.DrupalUsername,
|
||||||
|
instance.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, " ")
|
||||||
|
|
||||||
|
if st.Run(io, true, "barrel", "/bin/bash", "-c", provisionScript) != 0 {
|
||||||
|
return errors.New("Unable to run provision script")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (instance *Instance) NoPrefix() bool {
|
||||||
|
return fsx.IsFile(filepath.Join(instance.FilesystemBase, "prefixes.skip"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var errPrefixExecFailed = errors.New("PrefixConfig: Failed to call list_uri_prefixes")
|
||||||
|
|
||||||
|
// PrefixConfig returns the prefix config belonging to this instance.
|
||||||
|
func (instance *Instance) PrefixConfig() (config string, err error) {
|
||||||
|
// if the user requested to skip the prefix, then don't do anything with it!
|
||||||
|
if instance.NoPrefix() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// domain
|
||||||
|
builder.WriteString(instance.URL().String() + ":")
|
||||||
|
builder.WriteString("\n")
|
||||||
|
|
||||||
|
// default prefixes
|
||||||
|
wu := stream.NewIOStream(&builder, nil, nil, 0)
|
||||||
|
if instance.Stack().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/list_uri_prefixes.php") != 0 {
|
||||||
|
return "", errPrefixExecFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom prefixes
|
||||||
|
prefixPath := filepath.Join(instance.FilesystemBase, "prefixes")
|
||||||
|
if fsx.IsFile(prefixPath) {
|
||||||
|
prefix, err := os.Open(prefixPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer prefix.Close()
|
||||||
|
if _, err := io.Copy(&builder, prefix); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// and done!
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
96
env/instances_provision.go
vendored
Normal file
96
env/instances_provision.go
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/config"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/password"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (dis *Distillery) InstancesDir() string {
|
||||||
|
return filepath.Join(dis.Config.DeployRoot, "instances")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) InstanceDir(slug string) string {
|
||||||
|
return filepath.Join(dis.InstancesDir(), slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) InstanceSQL(slug string) (database, user string) {
|
||||||
|
database = dis.Config.MysqlDatabasePrefix + slug
|
||||||
|
user = dis.Config.MysqlUserPrefix + slug
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) InstanceGraphDB(slug string) (repo, user string) {
|
||||||
|
repo = dis.Config.GraphDBRepoPrefix + slug
|
||||||
|
user = dis.Config.GraphDBUserPrefix + slug
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password returns a new password
|
||||||
|
func (dis *Distillery) NewPassword() (value string, err error) {
|
||||||
|
return password.Password(dis.Config.PasswordLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errInvalidSlug = errors.New("Not a valid slug")
|
||||||
|
|
||||||
|
// NewInstance fills the struct for a new distillery 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.
|
||||||
|
func (dis *Distillery) NewInstance(slug string) (i Instance, err error) {
|
||||||
|
|
||||||
|
// make sure that the slug is valid!
|
||||||
|
if _, err := config.IsValidSlug(slug); err != nil {
|
||||||
|
return i, errInvalidSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate sql data
|
||||||
|
sqlPassword, err := dis.NewPassword()
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
sqlDB, sqlUser := dis.InstanceSQL(slug)
|
||||||
|
|
||||||
|
// generate ts data
|
||||||
|
tsPassword, err := dis.NewPassword()
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
tsRepo, tsUser := dis.InstanceGraphDB(slug)
|
||||||
|
|
||||||
|
// generate drupal data
|
||||||
|
drPassword, err := dis.NewPassword()
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
drUser := "admin"
|
||||||
|
|
||||||
|
// make the instance object!
|
||||||
|
instance := bookkeeping.Instance{
|
||||||
|
Slug: slug,
|
||||||
|
|
||||||
|
OwnerEmail: "",
|
||||||
|
AutoBlindUpdateEnabled: true,
|
||||||
|
|
||||||
|
FilesystemBase: dis.InstanceDir(slug),
|
||||||
|
|
||||||
|
SqlDatabase: sqlDB,
|
||||||
|
SqlUser: sqlUser,
|
||||||
|
SqlPassword: sqlPassword,
|
||||||
|
|
||||||
|
GraphDBRepository: tsRepo,
|
||||||
|
GraphDBUser: tsUser,
|
||||||
|
GraphDBPassword: tsPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
i.DrupalUsername = drUser
|
||||||
|
i.DrupalPassword = drPassword
|
||||||
|
|
||||||
|
// store the instance in the object and return it!
|
||||||
|
i.Instance = instance
|
||||||
|
i.dis = dis
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
92
env/params.go
vendored
Normal file
92
env/params.go
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params are parameters used for initialization of the environment
|
||||||
|
type Params struct {
|
||||||
|
BaseDirectory string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigFilePath returns the path to the configuration file
|
||||||
|
func (params Params) ConfigFilePath() string {
|
||||||
|
if params.BaseDirectory == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(params.BaseDirectory, ".env")
|
||||||
|
}
|
||||||
|
|
||||||
|
var errUnableToLoadParams = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneralArguments,
|
||||||
|
Message: "Unable to configure wdcli environment: %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseDirectoryDefault = "/var/www/deploy"
|
||||||
|
|
||||||
|
// ParamsFromEnv creates a new set of parameters from the environment.
|
||||||
|
// There is no guarantee that the parameters are valid.
|
||||||
|
func ParamsFromEnv() (params Params, err error) {
|
||||||
|
// try to read the base directory
|
||||||
|
value, err := ReadBaseDirectory()
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(err):
|
||||||
|
params.BaseDirectory = BaseDirectoryDefault
|
||||||
|
case err == nil:
|
||||||
|
params.BaseDirectory = value
|
||||||
|
default:
|
||||||
|
return params, errUnableToLoadParams.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseConfigFile = ".wdcli"
|
||||||
|
|
||||||
|
// ReadBaseDirectory reads the base directory from the environment, or an empty string
|
||||||
|
func ReadBaseDirectory() (value string, err error) {
|
||||||
|
// find the current user
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the base config file!
|
||||||
|
contents, err := os.ReadFile(filepath.Join(usr.HomeDir, baseConfigFile))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// and trim the spaces!
|
||||||
|
value = strings.TrimSpace(string(contents))
|
||||||
|
|
||||||
|
// check that it is actually set!
|
||||||
|
if len(value) == 0 {
|
||||||
|
return "", errors.New("ReadBaseDirectory: Directory is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// and return it!
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBaseDirectory writes the base directory to the environment, or returns an error
|
||||||
|
func WriteBaseDirectory(dir string) error {
|
||||||
|
// find the current user
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the base config file!
|
||||||
|
return os.WriteFile(
|
||||||
|
filepath.Join(usr.HomeDir, baseConfigFile),
|
||||||
|
[]byte(dir),
|
||||||
|
os.ModePerm,
|
||||||
|
)
|
||||||
|
}
|
||||||
24
env/requirements.go
vendored
Normal file
24
env/requirements.go
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tkw1536/goprogram"
|
||||||
|
"github.com/tkw1536/goprogram/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Requirements struct {
|
||||||
|
NeedsConfig bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowsFlag checks if the provided flag may be passed to fullfill this requirement
|
||||||
|
// By default it is used only for help page generation, and may be inaccurate.
|
||||||
|
func (r Requirements) AllowsFlag(flag meta.Flag) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates if this requirement is fullfilled for the provided global flags.
|
||||||
|
// It should return either nil, or an error of type exit.Error.
|
||||||
|
//
|
||||||
|
// Validate does not take into account AllowsOption, see ValidateAllowedOptions.
|
||||||
|
func (r Requirements) Validate(arguments goprogram.Arguments[struct{}]) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
env/stack.go
vendored
Normal file
30
env/stack.go
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stacks returns the Stacks of this distillery
|
||||||
|
func (dis *Distillery) Stacks() []stack.Installable {
|
||||||
|
// TODO: Do we want to cache these stacks?
|
||||||
|
return []stack.Installable{
|
||||||
|
dis.WebStack(),
|
||||||
|
dis.SelfStack(),
|
||||||
|
dis.ResolverStack(),
|
||||||
|
dis.SSHStack(),
|
||||||
|
dis.TriplestoreStack(),
|
||||||
|
dis.SQLStack(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// asCoreStack treats the provided stack as a core component of this distillery.
|
||||||
|
func (dis *Distillery) asCoreStack(stack stack.Installable) stack.Installable {
|
||||||
|
stack.Dir = filepath.Join(dis.Config.DeployRoot, "core", stack.Name)
|
||||||
|
|
||||||
|
stack.ContextResource = filepath.Join("resources", "compose", stack.Name)
|
||||||
|
stack.EnvFileResource = filepath.Join("resources", "templates", "docker-env", stack.Name)
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}
|
||||||
39
env/stack_resolver.go
vendored
Normal file
39
env/stack_resolver.go
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ResolverPrefixFile = "prefix.cfg"
|
||||||
|
|
||||||
|
func (dis *Distillery) ResolverStack() stack.Installable {
|
||||||
|
stack := dis.asCoreStack(stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "resolver",
|
||||||
|
},
|
||||||
|
|
||||||
|
EnvFileContext: map[string]string{
|
||||||
|
"VIRTUAL_HOST": dis.DefaultVirtualHost(),
|
||||||
|
"LETSENCRYPT_HOST": dis.DefaultLetsencryptHost(),
|
||||||
|
"LETSENCRYPT_EMAIL": dis.Config.CertbotEmail,
|
||||||
|
"PREFIX_FILE": "", // set below!
|
||||||
|
"DEFAULT_DOMAIN": dis.Config.DefaultDomain,
|
||||||
|
"LEGACY_DOMAIN": strings.Join(dis.Config.SelfExtraDomains, ","),
|
||||||
|
},
|
||||||
|
|
||||||
|
TouchFiles: []string{ResolverPrefixFile},
|
||||||
|
})
|
||||||
|
stack.EnvFileContext["PREFIX_FILE"] = filepath.Join(stack.Dir, ResolverPrefixFile)
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) ResolverStackPath() string {
|
||||||
|
return dis.ResolverStack().Dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis Distillery) ResolverPrefixConfig() string {
|
||||||
|
return filepath.Join(dis.ResolverStackPath(), ResolverPrefixFile)
|
||||||
|
}
|
||||||
28
env/stack_self.go
vendored
Normal file
28
env/stack_self.go
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import "github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
|
||||||
|
func (dis *Distillery) SelfStack() stack.Installable {
|
||||||
|
TARGET := "https://github.com/FAU-CDI/wisski-distillery"
|
||||||
|
if dis.Config.SelfRedirect != nil {
|
||||||
|
TARGET = dis.Config.SelfRedirect.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return dis.asCoreStack(stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "self",
|
||||||
|
},
|
||||||
|
|
||||||
|
EnvFileContext: map[string]string{
|
||||||
|
"VIRTUAL_HOST": dis.DefaultVirtualHost(),
|
||||||
|
"LETSENCRYPT_HOST": dis.DefaultLetsencryptHost(),
|
||||||
|
"LETSENCRYPT_EMAIL": dis.Config.CertbotEmail,
|
||||||
|
"TARGET": TARGET,
|
||||||
|
"OVERRIDES_FILE": dis.Config.SelfOverridesFile,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) SelfStackPath() string {
|
||||||
|
return dis.SelfStack().Dir
|
||||||
|
}
|
||||||
221
env/stack_sql.go
vendored
Normal file
221
env/stack_sql.go
vendored
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/sqle"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLStack returns the docker stack that handles the sql database.
|
||||||
|
func (dis *Distillery) SQLStack() stack.Installable {
|
||||||
|
return dis.asCoreStack(stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "sql",
|
||||||
|
},
|
||||||
|
|
||||||
|
MakeDirsPerm: fs.ModeDir | fs.ModePerm,
|
||||||
|
MakeDirs: []string{
|
||||||
|
"data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLStackPath returns the path the SQLStack() lives at.
|
||||||
|
func (dis *Distillery) SQLStackPath() string {
|
||||||
|
return dis.SQLStack().Dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqlOpen opens a new sql connection to the provided database using the administrative credentials
|
||||||
|
func (env Distillery) sqlOpen(database string, config *gorm.Config) (*gorm.DB, error) {
|
||||||
|
sql := mysql.Config{
|
||||||
|
DSN: fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", env.Config.MysqlAdminUser, env.Config.MysqlAdminPassword, "127.0.0.1:3306", database),
|
||||||
|
DefaultStringSize: 256,
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(mysql.New(sql), config)
|
||||||
|
if err != nil {
|
||||||
|
return db, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gdb, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return db, err
|
||||||
|
}
|
||||||
|
gdb.SetMaxIdleConns(0)
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errSQL = exit.Error{
|
||||||
|
Message: "error querying sql database: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqlBkTable returns a gorm connection to the bookkeeping database.
|
||||||
|
func (dis *Distillery) sqlBkTable(silent bool) (*gorm.DB, error) {
|
||||||
|
|
||||||
|
config := &gorm.Config{}
|
||||||
|
if silent {
|
||||||
|
config.Logger = logger.Default.LogMode(logger.Silent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the database
|
||||||
|
db, err := dis.sqlOpen(dis.Config.DistilleryBookkeepingDatabase, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errSQL.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the table
|
||||||
|
table := db.Table(dis.Config.DistilleryBookkeepingTable)
|
||||||
|
if table.Error != nil {
|
||||||
|
return nil, errSQL.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLShell executes a mysql shell inside the SQLStack.
|
||||||
|
func (dis *Distillery) SQLShell(io stream.IOStream, argv ...string) int {
|
||||||
|
return dis.SQLStack().Exec(io, "sql", "mysql", argv...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errSQLBootstrap = exit.Error{
|
||||||
|
Message: "Unable to boostrap SQL: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitSQLInterval = 1 * time.Second
|
||||||
|
|
||||||
|
// SQLWaitForShell waits for the sql database to be reachable via a docker-compose shell
|
||||||
|
func (dis *Distillery) SQLWaitForShell() error {
|
||||||
|
n := stream.FromNil()
|
||||||
|
return wait.Wait(func() bool {
|
||||||
|
return dis.SQLShell(n, "-e", "show databases;") == 0
|
||||||
|
}, waitSQLInterval, dis.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLWaitForConnection waits for the sql connection to be alive
|
||||||
|
func (dis *Distillery) SQLWaitForConnection() error {
|
||||||
|
return wait.Wait(func() bool {
|
||||||
|
_, err := dis.sqlBkTable(true)
|
||||||
|
return err == nil
|
||||||
|
}, waitSQLInterval, dis.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
var errInvalidDatabaseName = errors.New("SQLProvision: Invalid database name")
|
||||||
|
|
||||||
|
func (dis *Distillery) sqlRaw(query string, args ...interface{}) bool {
|
||||||
|
sql := sqle.Format(query, args...)
|
||||||
|
return dis.SQLShell(stream.FromNil(), "-e", sql) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLProvision provisions a new sql database and user
|
||||||
|
func (dis *Distillery) SQLProvision(name, user, password string) error {
|
||||||
|
// wait for the database
|
||||||
|
if err := dis.SQLWaitForShell(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's not a safe database name!
|
||||||
|
if !sqle.IsSafeDatabaseName(name) {
|
||||||
|
return errInvalidDatabaseName
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the database and user!
|
||||||
|
if !dis.sqlRaw("CREATE DATABASE `"+name+"`; CREATE USER ?@`%` IDENTIFIED BY ?; GRANT ALL PRIVILEGES ON `"+name+"`.* TO ?@`%`; FLUSH PRIVILEGES;", user, password, user) {
|
||||||
|
return errors.New("SQLProvision: Failed to create user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// and done!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errSQLPurgeUser = exit.Error{
|
||||||
|
Message: "Unable to delete user",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLPurgeUser deletes the specified user from the database
|
||||||
|
func (dis *Distillery) SQLPurgeUser(user string) error {
|
||||||
|
if !dis.sqlRaw("DROP USER IF EXISTS ?@`%`; FLUSH PRIVILEGES; ", user) {
|
||||||
|
return errSQLPurgeUser
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errSQLPurgeDB = exit.Error{
|
||||||
|
Message: "Unable to delete database",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLPurgeDatabase deletes the specified db from the database
|
||||||
|
func (dis *Distillery) SQLPurgeDatabase(db string) error {
|
||||||
|
if !sqle.IsSafeDatabaseName(db) {
|
||||||
|
return errSQLPurgeDB
|
||||||
|
}
|
||||||
|
if !dis.sqlRaw("DROP DATABASE IF EXISTS `" + db + "`") {
|
||||||
|
return errSQLPurgeDB
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLBootstrap bootstraps the SQL database, and makes sure that the bookkeeping table is up-to-date
|
||||||
|
func (dis *Distillery) SQLBootstrap(io stream.IOStream) error {
|
||||||
|
if err := dis.SQLWaitForShell(); err != nil {
|
||||||
|
return errSQLBootstrap.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the admin user
|
||||||
|
logging.LogMessage(io, "Creating administrative user")
|
||||||
|
{
|
||||||
|
username := dis.Config.MysqlAdminUser
|
||||||
|
password := dis.Config.MysqlAdminPassword
|
||||||
|
if !dis.sqlRaw("CREATE USER IF NOT EXISTS ?@'%' IDENTIFIED BY ?; GRANT ALL PRIVILEGES ON *.* TO ?@`%` WITH GRANT OPTION; FLUSH PRIVILEGES;", username, password, username) {
|
||||||
|
return errSQLBootstrap.WithMessageF("Unable to create administrative user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the admin user
|
||||||
|
logging.LogMessage(io, "Creating sql database")
|
||||||
|
{
|
||||||
|
if !sqle.IsSafeDatabaseName(dis.Config.DistilleryBookkeepingDatabase) {
|
||||||
|
return errSQLBootstrap.WithMessageF("Unsafe database name")
|
||||||
|
}
|
||||||
|
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`;", dis.Config.DistilleryBookkeepingDatabase)
|
||||||
|
if !dis.sqlRaw(createDBSQL) {
|
||||||
|
return errSQLBootstrap.WithMessageF(createDBSQL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the database to come up
|
||||||
|
logging.LogMessage(io, "Waiting for database update to be complete")
|
||||||
|
dis.SQLWaitForConnection()
|
||||||
|
|
||||||
|
// open the database
|
||||||
|
logging.LogMessage(io, "Migrating bookkeeping table")
|
||||||
|
{
|
||||||
|
db, err := dis.sqlBkTable(false)
|
||||||
|
if err != nil {
|
||||||
|
return errSQLBootstrap.WithMessageF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.AutoMigrate(&bookkeeping.Instance{}); err != nil {
|
||||||
|
return errSQLBootstrap.WithMessageF(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
16
env/stack_ssh.go
vendored
Normal file
16
env/stack_ssh.go
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import "github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
|
||||||
|
func (dis *Distillery) SSHStack() stack.Installable {
|
||||||
|
// TODO: Ensure that .env is copied if needed
|
||||||
|
return dis.asCoreStack(stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "sql",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) SSHStackPath() string {
|
||||||
|
return dis.SSHStack().Dir
|
||||||
|
}
|
||||||
263
env/stack_triplestore.go
vendored
Normal file
263
env/stack_triplestore.go
vendored
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (dis *Distillery) TriplestoreStack() stack.Installable {
|
||||||
|
return dis.asCoreStack(stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "triplestore",
|
||||||
|
},
|
||||||
|
|
||||||
|
CopyContextFiles: []string{"graphdb.zip"},
|
||||||
|
|
||||||
|
MakeDirsPerm: fs.ModeDir | fs.ModePerm,
|
||||||
|
MakeDirs: []string{
|
||||||
|
filepath.Join("data", "data"),
|
||||||
|
filepath.Join("data", "work"),
|
||||||
|
filepath.Join("data", "logs"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) TriplestoreStackPath() string {
|
||||||
|
return dis.TriplestoreStack().Dir
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriplestoreUserPayload struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
AppSettings TriplestoreUserAppSettings `json:"appSettings"`
|
||||||
|
GrantedAuthorities []string `json:"grantedAuthorities"`
|
||||||
|
}
|
||||||
|
type TriplestoreUserAppSettings struct {
|
||||||
|
DefaultInference bool `json:"DEFAULT_INFERENCE"`
|
||||||
|
DefaultVisGraphSchema bool `json:"DEFAULT_VIS_GRAPH_SCHEMA"`
|
||||||
|
DefaultSameas bool `json:"DEFAULT_SAMEAS"`
|
||||||
|
IgnoreSharedQueries bool `json:"IGNORE_SHARED_QUERIES"`
|
||||||
|
ExecuteCount bool `json:"EXECUTE_COUNT"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var errTriplestoreBootstrap = exit.Error{
|
||||||
|
Message: "Unable to bootstrap Triplestore: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
const triplestoreBaseURL = "http://127.0.0.1:7200"
|
||||||
|
const waitTSInterval = 1 * time.Second
|
||||||
|
|
||||||
|
// triplestoreCall makes a request to the triplestore.
|
||||||
|
//
|
||||||
|
// When bodyName is non-empty, expect body to be a byte slice representing a multipart/form-data upload with the given name.
|
||||||
|
// When bodyName is empty, simply marshal body as application/json
|
||||||
|
func (dis *Distillery) triplestoreRequest(method, url string, body interface{}, bodyName string, accept string) (*http.Response, error) {
|
||||||
|
var reader io.Reader
|
||||||
|
|
||||||
|
var contentType string
|
||||||
|
|
||||||
|
// for "PUT" and "POST" we setup a body
|
||||||
|
if method == "PUT" || method == "POST" {
|
||||||
|
if bodyName != "" {
|
||||||
|
buffer := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(buffer)
|
||||||
|
contentType = writer.FormDataContentType()
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile(bodyName, "filename.txt")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
io.Copy(part, bytes.NewReader(body.([]byte)))
|
||||||
|
writer.Close()
|
||||||
|
reader = buffer
|
||||||
|
} else {
|
||||||
|
contentType = "application/json"
|
||||||
|
mbytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader = bytes.NewReader(mbytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the request object
|
||||||
|
req, err := http.NewRequest(method, triplestoreBaseURL+url, reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup configuration!
|
||||||
|
if accept != "" {
|
||||||
|
req.Header.Set("Accept", accept)
|
||||||
|
}
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(dis.Config.TriplestoreAdminUser, dis.Config.TriplestoreAdminPassword)
|
||||||
|
|
||||||
|
// and send it
|
||||||
|
return http.DefaultClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) TriplestoreWaitForConnection() error {
|
||||||
|
return wait.Wait(func() bool {
|
||||||
|
res, err := dis.triplestoreRequest("GET", "/rest/repositories", nil, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
return true
|
||||||
|
}, waitTSInterval, dis.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
var errTripleStoreFailedRepository = exit.Error{
|
||||||
|
Message: "Failed to create repository: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) TriplestoreProvision(name, domain, user, password string) error {
|
||||||
|
if err := dis.TriplestoreWaitForConnection(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the create repo request
|
||||||
|
createRepo, err := distillery.ReadTemplate(filepath.Join("resources", "templates", "repository", "graphdb-repo.ttl"), map[string]string{
|
||||||
|
"GRAPHDB_REPO": name,
|
||||||
|
"INSTANCE_DOMAIN": domain,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the create!
|
||||||
|
{
|
||||||
|
res, err := dis.triplestoreRequest("POST", "/rest/repositories", createRepo, "config", "")
|
||||||
|
if err != nil {
|
||||||
|
return errTripleStoreFailedRepository.WithMessageF(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
return errTripleStoreFailedRepository.WithMessageF("Repo create did not return status code 201")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the user and grant them access
|
||||||
|
{
|
||||||
|
res, err := dis.triplestoreRequest("POST", "/rest/security/users/"+user, TriplestoreUserPayload{
|
||||||
|
Password: password,
|
||||||
|
AppSettings: TriplestoreUserAppSettings{
|
||||||
|
DefaultInference: true,
|
||||||
|
DefaultVisGraphSchema: true,
|
||||||
|
DefaultSameas: true,
|
||||||
|
IgnoreSharedQueries: false,
|
||||||
|
ExecuteCount: true,
|
||||||
|
},
|
||||||
|
GrantedAuthorities: []string{
|
||||||
|
"ROLE_USER",
|
||||||
|
"READ_REPO_" + name,
|
||||||
|
"WRITE_REPO_" + name,
|
||||||
|
},
|
||||||
|
}, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return errTripleStoreFailedRepository.WithMessageF(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
return errTripleStoreFailedRepository.WithMessageF("User create did not return status code 201")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriplestorePurgeUser deletes the specified user from the triplestore
|
||||||
|
func (dis *Distillery) TriplestorePurgeUser(user string) error {
|
||||||
|
res, err := dis.triplestoreRequest("DELETE", "/rest/security/users/"+user, nil, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusNoContent {
|
||||||
|
return errors.Errorf("Delete returned code %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriplestorePurgeRepo deletes the specified repo from the triplestore
|
||||||
|
func (dis *Distillery) TriplestorePurgeRepo(repo string) error {
|
||||||
|
res, err := dis.triplestoreRequest("DELETE", "/rest/repositories/"+repo, nil, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return errors.Errorf("Delete returned code %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) TriplestoreBootstrap(io stream.IOStream) error {
|
||||||
|
logging.LogMessage(io, "Waiting for Triplestore")
|
||||||
|
if err := dis.TriplestoreWaitForConnection(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.LogMessage(io, "Resetting admin user password")
|
||||||
|
{
|
||||||
|
res, err := dis.triplestoreRequest("PUT", "/rest/security/users/"+dis.Config.TriplestoreAdminUser, TriplestoreUserPayload{
|
||||||
|
Password: dis.Config.TriplestoreAdminPassword,
|
||||||
|
AppSettings: TriplestoreUserAppSettings{
|
||||||
|
DefaultInference: true,
|
||||||
|
DefaultVisGraphSchema: true,
|
||||||
|
DefaultSameas: true,
|
||||||
|
IgnoreSharedQueries: false,
|
||||||
|
ExecuteCount: true,
|
||||||
|
},
|
||||||
|
GrantedAuthorities: []string{"ROLE_ADMIN"},
|
||||||
|
}, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return errTriplestoreBootstrap.WithMessageF(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
switch res.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
// we set the password => requests are unauthorized
|
||||||
|
// so we still need to enable security (see below!)
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
// a password is needed => security is already enabled.
|
||||||
|
// the password may or may not work, but that's a problem for later
|
||||||
|
logging.LogMessage(io, "Security is already enabled")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errTriplestoreBootstrap.WithMessageF("Unable to set administrative password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.LogMessage(io, "Enabling Triplestore security")
|
||||||
|
{
|
||||||
|
res, err := dis.triplestoreRequest("POST", "/rest/security", true, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return errTriplestoreBootstrap.WithMessageF(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return errTriplestoreBootstrap.WithMessageF("Unable to enable security")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
19
env/stack_web.go
vendored
Normal file
19
env/stack_web.go
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import "github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
|
||||||
|
func (dis *Distillery) WebStack() stack.Installable {
|
||||||
|
return dis.asCoreStack(stack.Installable{
|
||||||
|
Stack: stack.Stack{
|
||||||
|
Name: "web",
|
||||||
|
},
|
||||||
|
|
||||||
|
EnvFileContext: map[string]string{
|
||||||
|
"DEFAULT_HOST": dis.Config.DefaultDomain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dis *Distillery) WebStackPath() string {
|
||||||
|
return dis.WebStack().Dir
|
||||||
|
}
|
||||||
23
go.mod
Normal file
23
go.mod
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
module github.com/FAU-CDI/wisski-distillery
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Showmax/go-fqdn v1.0.0
|
||||||
|
github.com/alessio/shellescape v1.4.1
|
||||||
|
github.com/feiin/sqlstring v0.3.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/tkw1536/goprogram v0.0.9
|
||||||
|
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
||||||
|
gorm.io/driver/mysql v1.3.6
|
||||||
|
gorm.io/gorm v1.23.8
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 // indirect
|
||||||
|
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
|
||||||
|
)
|
||||||
30
go.sum
Normal file
30
go.sum
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
github.com/Showmax/go-fqdn v1.0.0 h1:0rG5IbmVliNT5O19Mfuvna9LL7zlHyRfsSvBPZmF9tM=
|
||||||
|
github.com/Showmax/go-fqdn v1.0.0/go.mod h1:SfrFBzmDCtCGrnHhoDjuvFnKsWjEQX/Q9ARZvOrJAko=
|
||||||
|
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||||
|
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||||
|
github.com/feiin/sqlstring v0.3.0 h1:iyPEFijI2BxpY2M+AuhIvdNManzXa2OwGzuPaEMLUgo=
|
||||||
|
github.com/feiin/sqlstring v0.3.0/go.mod h1:xpZTjVUw1nD3hMgF9SMRdPiooKSikLf4PS5j2NTn3RI=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/tkw1536/goprogram v0.0.9 h1:y5bAWbiVRc47TjvpVDmyMtp5CgJXz1ultLOq+v9tfsA=
|
||||||
|
github.com/tkw1536/goprogram v0.0.9/go.mod h1:rX9MKOpJ9qAu4jHV2+n64SKmm3c2D3Hh1V8zC1H3jB4=
|
||||||
|
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
|
||||||
|
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2 h1:fqTvyMIIj+HRzMmnzr9NtpHP6uVpvB5fkHcgPDC4nu8=
|
||||||
|
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||||
|
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
gorm.io/driver/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
|
||||||
|
gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||||
|
gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
|
||||||
|
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
68
internal/bookkeeping/bookkeeping.go
Normal file
68
internal/bookkeeping/bookkeeping.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Package bookkeeping implements reading and writing from the bookkeeping table
|
||||||
|
package bookkeeping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instance is a WissKI Instance inside the bookkeeping table.
|
||||||
|
// It does not represent a running instance; it does not perform any validation.
|
||||||
|
type Instance struct {
|
||||||
|
// NOTE: Modifying this struct requires a database migration.
|
||||||
|
// This should nnever be done unless you know what you're doing.
|
||||||
|
|
||||||
|
// Primary key for the instance
|
||||||
|
Pk uint `gorm:"column:pk;primaryKey"`
|
||||||
|
|
||||||
|
// time the instance was created
|
||||||
|
Created time.Time `gorm:"column:created;autoCreateTime"`
|
||||||
|
|
||||||
|
// slug of the system
|
||||||
|
Slug string `gorm:"column:slug;not null;unique"`
|
||||||
|
|
||||||
|
// email address of the system owner (if any)
|
||||||
|
OwnerEmail string `gorm:"column:owner_email;type:varchar(320)"`
|
||||||
|
|
||||||
|
// should we automatically enable updates for the system?
|
||||||
|
AutoBlindUpdateEnabled SQLBit1 `gorm:"column:auto_blind_update_enabled;default:1"`
|
||||||
|
|
||||||
|
// The filesystem path the system can be found under
|
||||||
|
FilesystemBase string `gorm:"column:filesystem_base;not null"`
|
||||||
|
|
||||||
|
// SQL Database credentials for the system
|
||||||
|
SqlDatabase string `gorm:"column:sql_database;not null"`
|
||||||
|
SqlUser string `gorm:"column:sql_user;not null"`
|
||||||
|
SqlPassword string `gorm:"column:sql_password;not null"`
|
||||||
|
|
||||||
|
// GraphDB Repository
|
||||||
|
GraphDBRepository string `gorm:"column:graphdb_repository;not null"`
|
||||||
|
GraphDBUser string `gorm:"column:graphdb_user;not null"`
|
||||||
|
GraphDBPassword string `gorm:"column:graphdb_password;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Instance) IsBlindUpdateEnabled() bool {
|
||||||
|
return bool(i.AutoBlindUpdateEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLBit1 implements a boolean as a BIT(1)
|
||||||
|
type SQLBit1 bool
|
||||||
|
|
||||||
|
func (sb SQLBit1) Value() (driver.Value, error) {
|
||||||
|
if sb {
|
||||||
|
return []byte{1}, nil
|
||||||
|
} else {
|
||||||
|
return []byte{0}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errBadBool = errors.New("SQLBit1: Database does not contain Bit(1)")
|
||||||
|
|
||||||
|
func (sb *SQLBit1) Scan(src interface{}) error {
|
||||||
|
if bytes, ok := src.([]byte); ok && len(bytes) == 1 {
|
||||||
|
*sb = bytes[0] == 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errBadBool
|
||||||
|
}
|
||||||
153
internal/config/config.go
Normal file
153
internal/config/config.go
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
// Package config implements reading and validating a WissKIDistillery configuration file.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the configuration of a distillery instance
|
||||||
|
type Config struct {
|
||||||
|
// Several docker-compose files are created to manage global services and the system itself.
|
||||||
|
// On top of this all real-system space will be created under this directory.
|
||||||
|
DeployRoot string `env:"DEPLOY_ROOT" default:"/var/www/deploy" validator:"is_valid_abspath"`
|
||||||
|
|
||||||
|
// Each created Drupal Instance corresponds to a single domain name.
|
||||||
|
// These domain names should either be a complete domain name or a sub-domain of a default domain.
|
||||||
|
// This setting configures the default domain-name to create subdomains of.
|
||||||
|
DefaultDomain string `env:"DEFAULT_DOMAIN" default:"localhost.kwarc.info" validator:"is_valid_domain"`
|
||||||
|
|
||||||
|
// By default, the default domain redirects to the distillery repository.
|
||||||
|
// If you want to change this, set an alternate domain name here.
|
||||||
|
SelfRedirect *url.URL `env:"SELF_REDIRECT" default:"" validator:"is_valid_https_url"`
|
||||||
|
|
||||||
|
// By default, only the 'self' domain above is caught.
|
||||||
|
// To catch additional domains, add them here (comma seperated)
|
||||||
|
SelfExtraDomains []string `env:"SELF_EXTRA_DOMAINS" default:"" validator:"is_valid_domains"`
|
||||||
|
|
||||||
|
// You can override individual URLS in the homepage
|
||||||
|
// Do this by adding URLs (without trailing '/'s) into a JSON file
|
||||||
|
SelfOverridesFile string `env:"SELF_OVERRIDES_FILE" default:"" validator:"is_valid_file"`
|
||||||
|
|
||||||
|
// The system can support setting up certificate(s) automatically.
|
||||||
|
// It can be enabled by setting an email for certbot certificates.
|
||||||
|
// This email address can be configured here.
|
||||||
|
CertbotEmail string `env:"CERTBOT_EMAIL" default:"" validator:"is_valid_email"`
|
||||||
|
|
||||||
|
// Maximum age for backup
|
||||||
|
MaxBackupAge int `env:"MAX_BACKUP_AGE" default:"" validator:"is_valid_number"`
|
||||||
|
|
||||||
|
// Each Drupal instance requires a corresponding system user, database users and databases.
|
||||||
|
// These are also set by the appropriate domain name.
|
||||||
|
// To differentiate them from other users of the system, these names can be prefixed.
|
||||||
|
// The prefix to use can be configured here.
|
||||||
|
// When changing these please consider that no system user may exist that has the same name as a mysql user.
|
||||||
|
// This is a MariaDB restriction.
|
||||||
|
MysqlUserPrefix string `env:"MYSQL_USER_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||||
|
MysqlDatabasePrefix string `env:"MYSQL_DATABASE_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||||
|
GraphDBUserPrefix string `env:"GRAPHDB_USER_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||||
|
GraphDBRepoPrefix string `env:"GRAPHDB_REPO_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||||
|
|
||||||
|
// In addition to the filesystem the WissKI distillery requires a single SQL table.
|
||||||
|
// It uses this database to store a list of installed things.
|
||||||
|
DistilleryBookkeepingDatabase string `env:"DISTILLERY_BOOKKEEPING_DATABASE" default:"distillery" validator:"is_valid_slug"`
|
||||||
|
DistilleryBookkeepingTable string `env:"DISTILLERY_BOOKKEEPING_TABLE" default:"distillery" validator:"is_valid_slug"`
|
||||||
|
|
||||||
|
// Various components use password-based-authentication.
|
||||||
|
// These passwords are generated automatically.
|
||||||
|
// This variable can be used to determine their length.
|
||||||
|
PasswordLength int `env:"PASSWORD_LENGTH" default:"64" validator:"is_valid_number"`
|
||||||
|
|
||||||
|
// A file to be used for global authorized_keys for the ssh server.
|
||||||
|
GlobalAuthorizedKeysFile string `env:"GLOBAL_AUTHORIZED_KEYS_FILE" default:"/distillery/authorized_keys" validator:"is_valid_file"`
|
||||||
|
|
||||||
|
// admin credentials for graphdb
|
||||||
|
TriplestoreAdminUser string `env:"GRAPHDB_ADMIN_USER" default:"admin" validator:"is_nonempty"`
|
||||||
|
TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD" default:"" validator:"is_nonempty"`
|
||||||
|
|
||||||
|
// admin credentials for the Mysql database
|
||||||
|
MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" validator:"is_nonempty"`
|
||||||
|
MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"admin" validator:"is_nonempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config Config) String() string {
|
||||||
|
values := &strings.Builder{}
|
||||||
|
|
||||||
|
vConfig := reflect.ValueOf(config)
|
||||||
|
tConfig := vConfig.Type()
|
||||||
|
|
||||||
|
// iterate over the types
|
||||||
|
numValues := tConfig.NumField()
|
||||||
|
for i := 0; i < numValues; i++ {
|
||||||
|
tField := tConfig.Field(i)
|
||||||
|
vField := vConfig.FieldByName(tField.Name)
|
||||||
|
|
||||||
|
fmt.Fprintf(values, "%s=%v\n", tField.Tag.Get("env"), vField.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) Unmarshal(src io.Reader) error {
|
||||||
|
// read all the values!
|
||||||
|
values, err := ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vConfig := reflect.ValueOf(config).Elem()
|
||||||
|
tConfig := vConfig.Type()
|
||||||
|
|
||||||
|
// iterate over the types
|
||||||
|
numValues := tConfig.NumField()
|
||||||
|
for i := 0; i < numValues; i++ {
|
||||||
|
tField := tConfig.Field(i)
|
||||||
|
vField := vConfig.FieldByName(tField.Name)
|
||||||
|
|
||||||
|
env := tField.Tag.Get("env")
|
||||||
|
dflt := tField.Tag.Get("default")
|
||||||
|
validator := tField.Tag.Get("validator")
|
||||||
|
|
||||||
|
// read the value with a default
|
||||||
|
value, ok := values[env]
|
||||||
|
if !ok || value == "" {
|
||||||
|
if dflt == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value = dflt
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the validator
|
||||||
|
vFunc, ok := knownValidators[validator]
|
||||||
|
if vFunc == nil || !ok {
|
||||||
|
return errors.Errorf("Unable to read %q refers to unknown validator %s", env, validator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the parsed value
|
||||||
|
checked, err := vFunc(value)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Unable to read %q: Validator %s", env, validator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the value of the field
|
||||||
|
var errSet interface{}
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
errSet = recover()
|
||||||
|
}()
|
||||||
|
vField.Set(reflect.ValueOf(checked))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// capture any error
|
||||||
|
if errSet != nil {
|
||||||
|
return errors.Errorf("Unable to parse %q: validator %s returned %q", tField.Name, validator, errSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
internal/config/file.go
Normal file
76
internal/config/file.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scanner scans an io.Reader for a source file
|
||||||
|
type Scanner struct {
|
||||||
|
src *bufio.Scanner
|
||||||
|
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScanner(r io.Reader) *Scanner {
|
||||||
|
return &Scanner{
|
||||||
|
src: bufio.NewScanner(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanner advances the scanner to the next variable
|
||||||
|
func (scanner *Scanner) Scan() bool {
|
||||||
|
for scanner.src.Scan() {
|
||||||
|
// check that we don't have an empty or comment only line
|
||||||
|
tokens := strings.TrimSpace(scanner.src.Text())
|
||||||
|
if len(tokens) == 0 || tokens[0] == '#' || strings.HasPrefix(tokens, "//") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we have a 'key=value' pair
|
||||||
|
values := strings.SplitN(tokens, "=", 2)
|
||||||
|
if len(values) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// got a key = value
|
||||||
|
scanner.key = strings.TrimSpace(values[0])
|
||||||
|
scanner.value = strings.TrimSpace(values[1])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
scanner.key = ""
|
||||||
|
scanner.value = ""
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data reads the current value from the scanner.
|
||||||
|
// When Scan() has not been called, or returned false, returns two empty strings.
|
||||||
|
func (scanner Scanner) Data() (key, value string) {
|
||||||
|
return scanner.key, scanner.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns an error (if any)
|
||||||
|
func (scanner Scanner) Error() error {
|
||||||
|
return scanner.src.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll reads all key-value pairs from r.
|
||||||
|
// If a key occurs more than once, a later occurance overwrites a previous one.
|
||||||
|
func ReadAll(r io.Reader) (values map[string]string, err error) {
|
||||||
|
scanner := NewScanner(r)
|
||||||
|
|
||||||
|
// read and store all values
|
||||||
|
values = make(map[string]string)
|
||||||
|
for scanner.Scan() {
|
||||||
|
key, value := scanner.Data()
|
||||||
|
values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there was an error!
|
||||||
|
if err := scanner.Error(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
105
internal/config/validators.go
Normal file
105
internal/config/validators.go
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator reads from the configuration file
|
||||||
|
type Validator func(s string) (interface{}, error)
|
||||||
|
|
||||||
|
var knownValidators map[string]Validator = map[string]Validator{
|
||||||
|
"is_valid_abspath": IsValidAbspath,
|
||||||
|
"is_valid_domain": IsValidDomain,
|
||||||
|
"is_valid_domains": IsValidDomains,
|
||||||
|
"is_valid_number": IsValidNumber,
|
||||||
|
"is_valid_https_url": IsValidHttpsURL,
|
||||||
|
"is_valid_slug": IsValidSlug,
|
||||||
|
"is_valid_file": IsValidFile,
|
||||||
|
"is_valid_email": IsValidEmail,
|
||||||
|
"is_nonempty": IsNonEmpty,
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidAbspath(s string) (interface{}, error) {
|
||||||
|
if !fsx.IsDirectory(s) {
|
||||||
|
return nil, errors.Errorf("%q does not exist or is not a directory", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidFile(s string) (interface{}, error) {
|
||||||
|
if !fsx.IsFile(s) {
|
||||||
|
return nil, errors.Errorf("%q does not exist or is not a regular file", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNonEmpty(s string) (interface{}, error) {
|
||||||
|
if s == "" {
|
||||||
|
return nil, errors.New("value is empty")
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var regexpDomain = regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||||
|
|
||||||
|
func IsValidDomain(s string) (interface{}, error) {
|
||||||
|
if !regexpDomain.MatchString(s) {
|
||||||
|
return nil, errors.Errorf("%q is not a valid domain", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
func IsValidDomains(s string) (interface{}, error) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
domains := strings.Split(s, ",")
|
||||||
|
for _, d := range domains {
|
||||||
|
if !regexpDomain.MatchString(d) {
|
||||||
|
return nil, errors.Errorf("%q is not a valid domain", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domains, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidNumber(s string) (interface{}, error) {
|
||||||
|
value, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
return int(value), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidHttpsURL(s string) (interface{}, error) {
|
||||||
|
url, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%q is not a valid URL", s)
|
||||||
|
}
|
||||||
|
if url.Scheme != "https" {
|
||||||
|
return nil, errors.Errorf("%q is not a valid https URL (%q)", s, url.Scheme)
|
||||||
|
}
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var regexpEmail = regexp.MustCompile(`^([-a-zA-Z0-9]+)\@([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||||
|
|
||||||
|
func IsValidEmail(s string) (interface{}, error) {
|
||||||
|
if s == "" { // no email provided
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if !regexpEmail.MatchString(s) {
|
||||||
|
return nil, errors.Errorf("%q is not a valid email", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var regexpSlug = regexp.MustCompile(`^[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||||
|
|
||||||
|
func IsValidSlug(s string) (interface{}, error) {
|
||||||
|
if !regexpSlug.MatchString(s) {
|
||||||
|
return nil, errors.Errorf("%q is not a valid slug", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
4
internal/docs.go
Normal file
4
internal/docs.go
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Package internal contains various utility functions.
|
||||||
|
//
|
||||||
|
// These are not subject to version guarantees and may be changed
|
||||||
|
package internal
|
||||||
11
internal/execx/compose.go
Normal file
11
internal/execx/compose.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package execx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compose runs a docker-compose command in a specific directory, with the provided arguments and streams.
|
||||||
|
// It then waits for the process to exit, and returns the exit code.
|
||||||
|
func Compose(io stream.IOStream, workdir string, args ...string) int {
|
||||||
|
return Exec(io, workdir, "docker", append([]string{"compose"}, args...)...)
|
||||||
|
}
|
||||||
46
internal/execx/exec.go
Normal file
46
internal/execx/exec.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Package execx defines extensions to the "os/exec" package
|
||||||
|
package execx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecCommandError is returned by Exec when a command could not be executed.
|
||||||
|
// This typically hints that the executable cannot be found, but may have other causes.
|
||||||
|
const ExecCommandError = 127
|
||||||
|
|
||||||
|
// Exec executes a system command with the specified input/output streams, working directory, and arguments.
|
||||||
|
//
|
||||||
|
// If the command executes, it's exit code will be returned.
|
||||||
|
// If the command can not be executed, returns [ExecCommandError].
|
||||||
|
func Exec(io stream.IOStream, workdir string, exe string, argv ...string) int {
|
||||||
|
// setup the command
|
||||||
|
cmd := exec.Command(exe, argv...)
|
||||||
|
cmd.Dir = workdir
|
||||||
|
cmd.Stdin = io.Stdin
|
||||||
|
cmd.Stdout = io.Stdout
|
||||||
|
cmd.Stderr = io.Stderr
|
||||||
|
|
||||||
|
// run it
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
// non-zero exit
|
||||||
|
if err, ok := err.(*exec.ExitError); ok {
|
||||||
|
return err.ExitCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown error
|
||||||
|
if err != nil {
|
||||||
|
return ExecCommandError
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything is fine!
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustExec is like Exec, except that it returns true if the command exited successfully, and else false.
|
||||||
|
func MustExec(io stream.IOStream, workdir string, exe string, argv ...string) bool {
|
||||||
|
return Exec(io, workdir, exe, argv...) == 0
|
||||||
|
}
|
||||||
41
internal/fsx/copy.go
Normal file
41
internal/fsx/copy.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package fsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCopySameFile = errors.New("src and dst must be different files")
|
||||||
|
|
||||||
|
// CopyFile copies a file from src to dst.
|
||||||
|
// When dst and src are the same file, returns ErrCopySameFile.
|
||||||
|
func CopyFile(dst, src string) error {
|
||||||
|
if src == dst {
|
||||||
|
return ErrCopySameFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the source
|
||||||
|
srcFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// stat it to get the mode!
|
||||||
|
srcStat, err := srcFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// open or create the destination
|
||||||
|
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, srcStat.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
// and do the copy!
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
return err
|
||||||
|
}
|
||||||
25
internal/fsx/touch.go
Normal file
25
internal/fsx/touch.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package fsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Touch touches a file
|
||||||
|
func Touch(path string) error {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(err):
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return nil
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
now := time.Now().Local()
|
||||||
|
return os.Chtimes(path, now, now)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
internal/fsx/type.go
Normal file
13
internal/fsx/type.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package fsx
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func IsDirectory(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && info.Mode().IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFile(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && info.Mode().IsRegular()
|
||||||
|
}
|
||||||
32
internal/hostname/hostname.go
Normal file
32
internal/hostname/hostname.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Package hostname provides hostname
|
||||||
|
package hostname
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/Showmax/go-fqdn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FQDN attempts to return the fully qualified domain name of the host system.
|
||||||
|
// If an error occurs, may fall back to the empty string.
|
||||||
|
func FQDN() string {
|
||||||
|
|
||||||
|
// try the hostname function
|
||||||
|
{
|
||||||
|
fqdn, err := fqdn.FqdnHostname()
|
||||||
|
if err == nil {
|
||||||
|
return fqdn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to os hostname
|
||||||
|
{
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err == nil {
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the empty string
|
||||||
|
return ""
|
||||||
|
}
|
||||||
4
internal/legal/legal.go
Normal file
4
internal/legal/legal.go
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Package legal contains legal notices.
|
||||||
|
package legal
|
||||||
|
|
||||||
|
//go:generate gogenlicense -m
|
||||||
616
internal/legal/legal_notices.go
Executable file
616
internal/legal/legal_notices.go
Executable file
File diff suppressed because one or more lines are too long
59
internal/logging/level.go
Normal file
59
internal/logging/level.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logLevelMutex sync.Mutex
|
||||||
|
var logLevelMap = make(map[uintptr]int)
|
||||||
|
|
||||||
|
func getIndent(io stream.IOStream) int {
|
||||||
|
logLevelMutex.Lock()
|
||||||
|
defer logLevelMutex.Unlock()
|
||||||
|
|
||||||
|
id, ok := logID(io)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return logLevelMap[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func incIndent(io stream.IOStream) int {
|
||||||
|
logLevelMutex.Lock()
|
||||||
|
defer logLevelMutex.Unlock()
|
||||||
|
|
||||||
|
id, ok := logID(io)
|
||||||
|
if !ok { // if we don't have an id, then inc statically returns 1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevelMap[id]++
|
||||||
|
return logLevelMap[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func decIndent(io stream.IOStream) int {
|
||||||
|
logLevelMutex.Lock()
|
||||||
|
defer logLevelMutex.Unlock()
|
||||||
|
id, ok := logID(io)
|
||||||
|
|
||||||
|
if !ok { // if we don't have an id, then dec statically returns 0
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevelMap[id]--
|
||||||
|
if logLevelMap[id] < 0 {
|
||||||
|
panic("DecLogIdent: decrease below 0")
|
||||||
|
}
|
||||||
|
return logLevelMap[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func logID(io stream.IOStream) (uintptr, bool) {
|
||||||
|
file, ok := io.Stdin.(interface{ Fd() uintptr })
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return file.Fd(), true
|
||||||
|
}
|
||||||
30
internal/logging/log.go
Normal file
30
internal/logging/log.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogOperation logs a message that is displayed to the user, and then increases the log indent level.
|
||||||
|
func LogOperation(operation func() error, io stream.IOStream, format string, args ...interface{}) error {
|
||||||
|
logOperation(io, getIndent(io), format, args...)
|
||||||
|
incIndent(io)
|
||||||
|
defer decIndent(io)
|
||||||
|
|
||||||
|
return operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogMessage logs a message that is displayed to the user
|
||||||
|
func LogMessage(io stream.IOStream, format string, args ...interface{}) (int, error) {
|
||||||
|
return logOperation(io, getIndent(io), format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logOperation(io stream.IOStream, indent int, format string, args ...interface{}) (int, error) {
|
||||||
|
message := "\033[1m" + strings.Repeat(" ", indent+1) + "=> " + format + "\033[0m\n"
|
||||||
|
if !io.StdinIsATerminal() {
|
||||||
|
message = " => " + format
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.Printf(message, args...)
|
||||||
|
}
|
||||||
41
internal/password/password.go
Normal file
41
internal/password/password.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Package password allows generating random passwords
|
||||||
|
package password
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE(twiesing): A bunch of scripts cannot properly handle the extra characters in the password.
|
||||||
|
// For now it is disabled, but it should be re-enabled later.
|
||||||
|
const PasswordCharSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // + "!@#$%&*"
|
||||||
|
const PasswordCharCount = len(PasswordCharSet)
|
||||||
|
|
||||||
|
// Password returns a randomly generated password with the provided length.
|
||||||
|
// [rand.Reader] is used as the source of randomness.
|
||||||
|
func Password(length int) (string, error) {
|
||||||
|
if length < 0 {
|
||||||
|
panic("length < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
var password strings.Builder
|
||||||
|
password.Grow(length)
|
||||||
|
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
|
||||||
|
// grab a random index!
|
||||||
|
index, err := rand.Int(rand.Reader, big.NewInt(int64(PasswordCharCount)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// and use that index!
|
||||||
|
if err := password.WriteByte(PasswordCharSet[int(index.Int64())]); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the password!
|
||||||
|
return password.String(), nil
|
||||||
|
}
|
||||||
39
internal/sqle/name.go
Normal file
39
internal/sqle/name.go
Normal file
File diff suppressed because one or more lines are too long
10
internal/sqle/sqle.go
Normal file
10
internal/sqle/sqle.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package sqle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/feiin/sqlstring"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format formats the provided query with the given parameters.
|
||||||
|
func Format(query string, params ...interface{}) string {
|
||||||
|
return sqlstring.Format(query, params...)
|
||||||
|
}
|
||||||
108
internal/stack/installable.go
Normal file
108
internal/stack/installable.go
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Installable represents a Stack that can be automatically installed from a set of resources
|
||||||
|
// See the Install() method.
|
||||||
|
type Installable struct {
|
||||||
|
Stack
|
||||||
|
|
||||||
|
ContextResource string // Path to the resource containing 'docker compose' context
|
||||||
|
|
||||||
|
EnvFileResource string // Path to the resource containing dynamically generated env file
|
||||||
|
EnvFileContext map[string]string // Context of variables to replace in the env file
|
||||||
|
|
||||||
|
CopyContextFiles []string // Files to copy from the installation context
|
||||||
|
|
||||||
|
TouchFiles []string // Files to 'touch', i.e. ensure that exist
|
||||||
|
|
||||||
|
MakeDirsPerm fs.FileMode // permission for diretories, defaults to fs.ModeDir
|
||||||
|
MakeDirs []string // directories to ensure that exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallationContext is a context to install data in
|
||||||
|
type InstallationContext map[string]string
|
||||||
|
|
||||||
|
// Install installs or updates this stack into the directory specified by stack.Stack().
|
||||||
|
//
|
||||||
|
// Installation is non-interactive, but will provide debugging output onto io.
|
||||||
|
// InstallationContext
|
||||||
|
func (is Installable) Install(io stream.IOStream, context InstallationContext) error {
|
||||||
|
// setup the base files
|
||||||
|
if err := distillery.InstallResource(
|
||||||
|
is.Dir,
|
||||||
|
is.ContextResource,
|
||||||
|
func(dst, src string) {
|
||||||
|
io.Printf("[install] %s\n", dst)
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure .env
|
||||||
|
envDest := filepath.Join(is.Dir, ".env")
|
||||||
|
if is.EnvFileResource != "" && is.EnvFileContext != nil {
|
||||||
|
io.Printf("[config] %s\n", envDest)
|
||||||
|
if err := distillery.InstallTemplate(
|
||||||
|
envDest,
|
||||||
|
is.EnvFileResource,
|
||||||
|
is.EnvFileContext,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that certain files exist
|
||||||
|
for _, name := range is.MakeDirs {
|
||||||
|
// find the destination!
|
||||||
|
dst := filepath.Join(is.Dir, name)
|
||||||
|
|
||||||
|
io.Printf("[make] %s\n", dst)
|
||||||
|
if is.MakeDirsPerm == fs.FileMode(0) {
|
||||||
|
is.MakeDirsPerm = fs.ModeDir
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dst, is.MakeDirsPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy files from the context!
|
||||||
|
for _, name := range is.CopyContextFiles {
|
||||||
|
// find the source!
|
||||||
|
src, ok := context[name]
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("Missing file from context: %s", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the destination!
|
||||||
|
dst := filepath.Join(is.Dir, name)
|
||||||
|
|
||||||
|
// copy over file from context
|
||||||
|
io.Printf("[copy] %s (from %s)\n", dst, src)
|
||||||
|
if err := fsx.CopyFile(dst, src); err != nil {
|
||||||
|
return errors.Wrapf(err, "Unable to copy file %s", src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that certain files exist
|
||||||
|
for _, name := range is.TouchFiles {
|
||||||
|
// find the destination!
|
||||||
|
dst := filepath.Join(is.Dir, name)
|
||||||
|
|
||||||
|
io.Printf("[touch] %s\n", dst)
|
||||||
|
if err := fsx.Touch(dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
110
internal/stack/stack.go
Normal file
110
internal/stack/stack.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Package stack implements a docker compose stack
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/execx"
|
||||||
|
"github.com/tkw1536/goprogram/stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stack represents a 'docker compose' stack living in a specific directory
|
||||||
|
//
|
||||||
|
// NOTE(twiesing): In the current implementation this requires a 'docker' executable on the system.
|
||||||
|
// This executable must be capable of the 'docker compose' command.
|
||||||
|
// In the future the idea is to replace this with a native docker compose client.
|
||||||
|
type Stack struct {
|
||||||
|
Name string // Name of this stack, TODO: Do we need this?
|
||||||
|
Dir string // Directory of this stack
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStackUpdatePull = errors.New("Stack.Update: Pull returned non-zero exit code")
|
||||||
|
var errStackUpdateBuild = errors.New("Stack.Update: Build returned non-zero exit code")
|
||||||
|
|
||||||
|
// Update pulls, builds, and then optionally starts this stack.
|
||||||
|
// This does not have a direct 'docker compose' shell equivalent.
|
||||||
|
//
|
||||||
|
// See also Up.
|
||||||
|
func (ds Stack) Update(io stream.IOStream, start bool) error {
|
||||||
|
if ds.compose(io, "pull") != 0 {
|
||||||
|
return errStackUpdatePull
|
||||||
|
}
|
||||||
|
if ds.compose(io, "build", "--pull") != 0 {
|
||||||
|
return errStackUpdateBuild
|
||||||
|
}
|
||||||
|
if start {
|
||||||
|
return ds.Up(io)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStackUp = errors.New("Stack.Up: Up returned non-zero exit code")
|
||||||
|
|
||||||
|
// Up creates and starts the containers in this Stack.
|
||||||
|
// It is equivalent to 'docker compose up -d' on the shell.
|
||||||
|
func (ds Stack) Up(io stream.IOStream) error {
|
||||||
|
if ds.compose(io, "up", "-d") != 0 {
|
||||||
|
return errStackUp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes an executable in the provided running service.
|
||||||
|
// It is equivalent to 'docker compose exec $service $executable $args...'.
|
||||||
|
//
|
||||||
|
// It returns the exit code of the process.
|
||||||
|
func (ds Stack) Exec(io stream.IOStream, service, executable string, args ...string) int {
|
||||||
|
compose := []string{"exec"}
|
||||||
|
if io.StdinIsATerminal() {
|
||||||
|
compose = append(compose, "-ti")
|
||||||
|
}
|
||||||
|
compose = append(compose, executable)
|
||||||
|
compose = append(compose, args...)
|
||||||
|
return ds.compose(io, compose...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the provided service with the given executable.
|
||||||
|
// It is equivalent to 'docker compose run [--rm] $service $executable $args...'.
|
||||||
|
//
|
||||||
|
// It returns the exit code of the process.
|
||||||
|
func (ds Stack) Run(io stream.IOStream, autoRemove bool, service, command string, args ...string) int {
|
||||||
|
compose := []string{"run"}
|
||||||
|
if autoRemove {
|
||||||
|
compose = append(compose, "--rm")
|
||||||
|
}
|
||||||
|
if !io.StdinIsATerminal() {
|
||||||
|
compose = append(compose, "-T")
|
||||||
|
}
|
||||||
|
compose = append(compose, command)
|
||||||
|
compose = append(compose, args...)
|
||||||
|
return ds.compose(io, compose...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStackRestart = errors.New("Stack.Restart: Restart returned non-zero exit code")
|
||||||
|
|
||||||
|
// Restart restarts all containers in this Stack.
|
||||||
|
// It is equivalent to 'docker compose restart' on the shell.
|
||||||
|
func (ds Stack) Restart(io stream.IOStream) error {
|
||||||
|
if ds.compose(io, "restart") != 0 {
|
||||||
|
return errStackRestart
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStackDown = errors.New("Stack.Down: Down returned non-zero exit code")
|
||||||
|
|
||||||
|
// Down stops and removes all containers in this Stack.
|
||||||
|
// It is equivalent to 'docker compose down -v' on the shell.
|
||||||
|
func (ds Stack) Down(io stream.IOStream) error {
|
||||||
|
if ds.compose(io, "down", "-v") != 0 {
|
||||||
|
return errStackDown
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose executes a 'docker compose' command on this stack.
|
||||||
|
// TODO: This should be removed and replaced by an internal call directly to libcompose.
|
||||||
|
func (ds Stack) compose(io stream.IOStream, args ...string) int {
|
||||||
|
// TODO: can we migrate to a built-in version of this?
|
||||||
|
return execx.Compose(io, ds.Dir, args...)
|
||||||
|
}
|
||||||
31
internal/wait/wait.go
Normal file
31
internal/wait/wait.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package wait
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait repeatedly invokes f, until it returns true or the context is closed.
|
||||||
|
// The invocation interval is determined by interval.
|
||||||
|
func Wait(f func() bool, interval time.Duration, context context.Context) error {
|
||||||
|
// create a new timer
|
||||||
|
timer := time.NewTimer(interval)
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if f() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the timer, and wait for it again!
|
||||||
|
timer.Reset(interval)
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
case <-context.Done():
|
||||||
|
return context.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
license.go
Normal file
6
license.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package wisski_distillery
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed LICENSE
|
||||||
|
var License string
|
||||||
44
program.go
Normal file
44
program.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package wisski_distillery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/user"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/env"
|
||||||
|
"github.com/tkw1536/goprogram"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// these define the ggman-specific program types
|
||||||
|
// none of these are strictly needed, they're just around for convenience
|
||||||
|
type wdcliEnv = *env.Distillery
|
||||||
|
type wdcliParameters = env.Params
|
||||||
|
type wdcliRequirements = env.Requirements
|
||||||
|
type wdCliFlags = struct{}
|
||||||
|
|
||||||
|
type Program = goprogram.Program[wdcliEnv, wdcliParameters, wdCliFlags, wdcliRequirements]
|
||||||
|
type Command = goprogram.Command[wdcliEnv, wdcliParameters, wdCliFlags, wdcliRequirements]
|
||||||
|
type Context = goprogram.Context[wdcliEnv, wdcliParameters, wdCliFlags, wdcliRequirements]
|
||||||
|
type Arguments = goprogram.Arguments[wdCliFlags]
|
||||||
|
type Description = goprogram.Description[wdCliFlags, wdcliRequirements]
|
||||||
|
|
||||||
|
// an error when nor arguments are provided.
|
||||||
|
var errUserIsNotRoot = exit.Error{
|
||||||
|
ExitCode: exit.ExitGeneralArguments,
|
||||||
|
Message: "This command has to be executed as root. The current user is not root.",
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgram() Program {
|
||||||
|
return Program{
|
||||||
|
BeforeCommand: func(context Context, command Command) error {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err != nil || usr.Uid != "0" || usr.Gid != "0" { // make sure that we are root!
|
||||||
|
return errUserIsNotRoot
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
NewEnvironment: func(params wdcliParameters, context Context) (e wdcliEnv, err error) {
|
||||||
|
return env.NewDistillery(params, context.Description.Requirements)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue