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:
Tom Wiesing 2022-08-14 10:57:59 +02:00
parent db2ad9b4bd
commit 7b38fdd801
No known key found for this signature in database
93 changed files with 4689 additions and 645 deletions

54
cmd/blind_update.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
`